From 8af416d04f14b0e766c664c2cc3f1f22be5276fd Mon Sep 17 00:00:00 2001 From: Long Zheng Date: Wed, 28 Aug 2024 18:34:16 +1000 Subject: [PATCH] Refactor MirrorUsagePoint helpers to base class --- src/sep2/helpers/mirrorUsagePointBase.ts | 219 +++++++++ src/sep2/helpers/mirrorUsagePointDer.ts | 432 +++++------------- src/sep2/helpers/mirrorUsagePointSite.ts | 543 ++++++----------------- 3 files changed, 458 insertions(+), 736 deletions(-) create mode 100644 src/sep2/helpers/mirrorUsagePointBase.ts diff --git a/src/sep2/helpers/mirrorUsagePointBase.ts b/src/sep2/helpers/mirrorUsagePointBase.ts new file mode 100644 index 0000000..0e901b6 --- /dev/null +++ b/src/sep2/helpers/mirrorUsagePointBase.ts @@ -0,0 +1,219 @@ +import { parseStringPromise } from 'xml2js'; +import { getMillisecondsToNextHourMinutesInterval } from '../../helpers/time'; +import type { SEP2Client } from '../client'; +import { defaultPollPushRates } from '../client'; +import type { MirrorMeterReading } from '../models/mirrorMeterReading'; +import { generateMirrorMeterReadingResponse } from '../models/mirrorMeterReading'; +import type { MirrorUsagePoint } from '../models/mirrorUsagePoint'; +import { + MirrorUsagePointStatus, + generateMirrorUsagePointResponse, + parseMirrorUsagePointXmlObject, +} from '../models/mirrorUsagePoint'; +import type { RoleFlagsType } from '../models/roleFlagsType'; +import { ServiceKind } from '../models/serviceKind'; +import { objectToXml } from './xml'; +import type { Logger } from 'pino'; + +export abstract class MirrorUsagePointHelperBase { + protected client: SEP2Client; + protected mirrorUsagePointListHref: string | null = null; + protected mirrorUsagePoint: MirrorUsagePoint | null = null; + protected samples: MonitoringSample[] = []; + protected abstract description: string; + protected abstract roleFlags: RoleFlagsType; + protected postTimer: NodeJS.Timeout | null = null; + protected abstract logger: Logger; + + constructor({ client }: { client: SEP2Client }) { + this.client = client; + } + + public async updateMirrorUsagePointList({ + mirrorUsagePoints, + mirrorUsagePointListHref, + }: { + mirrorUsagePoints: MirrorUsagePoint[]; + mirrorUsagePointListHref: string; + }) { + this.mirrorUsagePointListHref = mirrorUsagePointListHref; + + // use existing mirrorUsagePoint if it has already been created before + const existingMirrorUsagePoint = mirrorUsagePoints.find( + (point) => + point.deviceLFDI === this.client.lfdi && + point.status === MirrorUsagePointStatus.On && + point.roleFlags === this.roleFlags, + ); + + if (existingMirrorUsagePoint) { + this.mirrorUsagePoint = existingMirrorUsagePoint; + this.startPosting(); + return; + } + + // if does not exist, create new mirrorUsagePoint + this.mirrorUsagePoint = await this.postMirrorUsagePoint({ + mirrorUsagePoint: { + mRID: this.client.generateUsagePointMrid(this.roleFlags), + description: this.description, + roleFlags: this.roleFlags, + serviceCategoryKind: ServiceKind.Electricity, + status: MirrorUsagePointStatus.On, + deviceLFDI: this.client.lfdi, + }, + }); + + this.startPosting(); + } + + protected startPosting() { + if (this.postTimer) return; + + void this.post(); + } + + public addSample(sample: MonitoringSample) { + this.samples.push(sample); + } + + protected abstract getReadingFromSamples( + samples: MonitoringSample[], + ): Reading; + + public post() { + const nextUpdateMilliseconds = getMillisecondsToNextHourMinutesInterval( + (this.mirrorUsagePoint?.postRate ?? + defaultPollPushRates.mirrorUsagePointPush) / 60, + ); + + this.postTimer = setTimeout(() => { + void this.post(); + }, nextUpdateMilliseconds); + + const samples = this.getSamplesAndClear(); + + if (samples.length > 0) { + const reading = this.getReadingFromSamples(samples); + const lastUpdateTime = new Date(); + const nextUpdateTime = new Date( + Date.now() + nextUpdateMilliseconds, + ); + + try { + this.postRealPowerAverage({ + reading, + lastUpdateTime, + nextUpdateTime, + }); + + this.postReactivePowerAverage({ + reading, + lastUpdateTime, + nextUpdateTime, + }); + + this.postVoltageAverage({ + reading, + lastUpdateTime, + nextUpdateTime, + }); + + this.postFrequency({ + reading, + lastUpdateTime, + nextUpdateTime, + }); + } catch (error) { + this.logger.error( + { error }, + 'Error posting one of MirrorMeterReading during scheduled pushing', + ); + } + } + } + + protected abstract postRealPowerAverage({ + reading, + lastUpdateTime, + nextUpdateTime, + }: { + reading: Reading; + lastUpdateTime: Date; + nextUpdateTime: Date; + }): void; + + protected abstract postReactivePowerAverage({ + reading, + lastUpdateTime, + nextUpdateTime, + }: { + reading: Reading; + lastUpdateTime: Date; + nextUpdateTime: Date; + }): void; + + protected abstract postVoltageAverage({ + reading, + lastUpdateTime, + nextUpdateTime, + }: { + reading: Reading; + lastUpdateTime: Date; + nextUpdateTime: Date; + }): void; + + protected abstract postFrequency({ + reading, + lastUpdateTime, + nextUpdateTime, + }: { + reading: Reading; + lastUpdateTime: Date; + nextUpdateTime: Date; + }): void; + + protected getSamplesAndClear(): MonitoringSample[] { + const cache = this.samples; + this.samples = []; + return cache; + } + + protected async postMirrorUsagePoint({ + mirrorUsagePoint, + }: { + mirrorUsagePoint: MirrorUsagePoint; + }) { + if (!this.mirrorUsagePointListHref) { + throw new Error('Missing mirrorUsagePointHref'); + } + + const data = generateMirrorUsagePointResponse(mirrorUsagePoint); + const xml = objectToXml(data); + + const response = await this.client.post( + this.mirrorUsagePointListHref, + xml, + ); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument + const responseXml = await parseStringPromise(response.data); + + return parseMirrorUsagePointXmlObject(responseXml); + } + + protected async postMirrorMeterReading({ + mirrorMeterReading, + }: { + mirrorMeterReading: MirrorMeterReading; + }) { + if (!this.mirrorUsagePoint || !this.mirrorUsagePoint.href) { + throw new Error('Missing mirrorUsagePoint or its href'); + } + + const data = generateMirrorMeterReadingResponse(mirrorMeterReading); + const xml = objectToXml(data); + + await this.client.post(this.mirrorUsagePoint.href, xml); + } +} diff --git a/src/sep2/helpers/mirrorUsagePointDer.ts b/src/sep2/helpers/mirrorUsagePointDer.ts index e272037..7a493dd 100644 --- a/src/sep2/helpers/mirrorUsagePointDer.ts +++ b/src/sep2/helpers/mirrorUsagePointDer.ts @@ -1,19 +1,4 @@ -import { parseStringPromise } from 'xml2js'; -import { defaultPollPushRates, type SEP2Client } from '../client'; -import { - generateMirrorUsagePointResponse, - MirrorUsagePointStatus, - parseMirrorUsagePointXmlObject, - type MirrorUsagePoint, -} from '../models/mirrorUsagePoint'; import { RoleFlagsType } from '../models/roleFlagsType'; -import { objectToXml } from './xml'; -import { ServiceKind } from '../models/serviceKind'; -import { getMillisecondsToNextHourMinutesInterval } from '../../helpers/time'; -import { - generateMirrorMeterReadingResponse, - type MirrorMeterReading, -} from '../models/mirrorMeterReading'; import { getSamplesIntervalSeconds, type MonitoringSample, @@ -31,8 +16,10 @@ import { DataQualifierType } from '../models/dataQualifierType'; import { FlowDirectionType } from '../models/flowDirectionType'; import { PhaseCode } from '../models/phaseCode'; import { UomType } from '../models/uomType'; +import { MirrorUsagePointHelperBase } from './mirrorUsagePointBase'; +import { logger as pinoLogger } from '../../helpers/logger'; -type DerMonitoringSimple = Pick; +type DerMonitoringSample = Pick; type DerReading = { intervalSeconds: number; @@ -45,115 +32,55 @@ type DerReading = { }; }; -// TODO: refactor MirrorUsagePointSiteHelper and MirrorUsagePointSiteHelper to use a common base class with abstracts -export class MirrorUsagePointDerHelper { - private client: SEP2Client; - private mirrorUsagePointListHref: string | null = null; - private mirrorUsagePoint: MirrorUsagePoint | null = null; - private samples: DerMonitoringSimple[] = []; - private roleFlags: RoleFlagsType = +export class MirrorUsagePointDerHelper extends MirrorUsagePointHelperBase< + DerMonitoringSample, + DerReading +> { + protected roleFlags = RoleFlagsType.isDER | RoleFlagsType.isMirror | RoleFlagsType.isSubmeter; - private postTimer: NodeJS.Timeout | null = null; - - constructor({ client }: { client: SEP2Client }) { - this.client = client; - } - - public async updateMirrorUsagePointList({ - mirrorUsagePoints, - mirrorUsagePointListHref, - }: { - mirrorUsagePoints: MirrorUsagePoint[]; - mirrorUsagePointListHref: string; - }) { - this.mirrorUsagePointListHref = mirrorUsagePointListHref; - - // find existing relevant mirrorUsagePoint - const existingMirrorUsagePoint = mirrorUsagePoints.find((point) => { - return ( - point.deviceLFDI === this.client.lfdi && - point.status === MirrorUsagePointStatus.On && - point.roleFlags === this.roleFlags - ); - }); - - if (existingMirrorUsagePoint) { - this.mirrorUsagePoint = existingMirrorUsagePoint; - return; - } - - // if does not exist, create new mirrorUsagePoint - this.mirrorUsagePoint = await this.postMirrorUsagePoint({ - mirrorUsagePoint: { - mRID: this.client.generateUsagePointMrid(this.roleFlags), - description: 'DER measurement', - roleFlags: this.roleFlags, - serviceCategoryKind: ServiceKind.Electricity, - status: MirrorUsagePointStatus.On, - deviceLFDI: this.client.lfdi, + protected description = 'DER measurement'; + protected logger = pinoLogger.child({ + module: 'MirrorUsagePointDerHelper', + }); + + protected getReadingFromSamples( + samples: DerMonitoringSample[], + ): DerReading { + return { + intervalSeconds: getSamplesIntervalSeconds(samples), + realPowerAverage: { + phaseA: averageNumbersArray( + samples.map((s) => s.der.realPower.phaseA), + ), + phaseB: averageNumbersNullableArray( + samples.map((s) => s.der.realPower.phaseB), + ), + phaseC: averageNumbersNullableArray( + samples.map((s) => s.der.realPower.phaseC), + ), }, - }); - - // start posting - if (!this.postTimer) { - void this.post(); - } - } - - public addSample(sample: DerMonitoringSimple) { - this.samples.push(sample); - } - - public post() { - const nextUpdateMilliseconds = getMillisecondsToNextHourMinutesInterval( - // convert seconds to minutes - (this.mirrorUsagePoint?.postRate ?? - defaultPollPushRates.mirrorUsagePointPush) / 60, - ); - - // set up next interval - this.postTimer = setTimeout(() => { - void this.post(); - }, nextUpdateMilliseconds); - - const samples = this.getSamplesAndClear(); - - if (samples.length > 0) { - const reading = this.getReadingFromSamples(samples); - - const lastUpdateTime = new Date(); - - const nextUpdateTime = new Date( - Date.now() + nextUpdateMilliseconds, - ); - - this.postRealPowerAverage({ - reading, - lastUpdateTime, - nextUpdateTime, - }); - - this.postReactivePowerAverage({ - reading, - lastUpdateTime, - nextUpdateTime, - }); - - this.postVoltageAverage({ - reading, - lastUpdateTime, - nextUpdateTime, - }); - - this.postFrequency({ - reading, - lastUpdateTime, - nextUpdateTime, - }); - } + reactivePowerAverage: averageNumbersArray( + samples.map((s) => s.der.reactivePower), + ), + voltageAverage: { + phaseA: averageNumbersArray( + samples.map((s) => s.der.voltage.phaseA), + ), + phaseB: averageNumbersNullableArray( + samples.map((s) => s.der.voltage.phaseB), + ), + phaseC: averageNumbersNullableArray( + samples.map((s) => s.der.voltage.phaseC), + ), + }, + frequency: { + maximum: Math.max(...samples.map((s) => s.der.frequency)), + minimum: Math.min(...samples.map((s) => s.der.frequency)), + }, + }; } - private postRealPowerAverage({ + protected postRealPowerAverage({ reading, lastUpdateTime, nextUpdateTime, @@ -162,46 +89,21 @@ export class MirrorUsagePointDerHelper { lastUpdateTime: Date; nextUpdateTime: Date; }) { - const phaseAValue = convertNumberToBaseAndPow10Exponent( - reading.realPowerAverage.phaseA, - ); - - void this.postMirrorMeterReading({ - mirrorMeterReading: { - mRID: this.client.generateMeterReadingMrid(), - description: 'Average Real Power (W) - Phase A', - lastUpdateTime, - nextUpdateTime, - Reading: { - value: phaseAValue.base, - qualityFlags: QualityFlags.Valid, - }, - ReadingType: { - commodity: CommodityType.ElectricitySecondaryMeteredValue, - kind: KindType.Power, - dataQualifier: DataQualifierType.Average, - flowDirection: FlowDirectionType.Reverse, - intervalLength: reading.intervalSeconds, - phase: PhaseCode.PhaseA, - powerOfTenMultiplier: phaseAValue.pow10, - uom: UomType.W, - }, - }, - }); - - if (reading.realPowerAverage.phaseB) { - const phaseBValue = convertNumberToBaseAndPow10Exponent( - reading.realPowerAverage.phaseB, - ); + const postReading = ( + phase: PhaseCode, + description: string, + value: number, + ) => { + const phaseValue = convertNumberToBaseAndPow10Exponent(value); void this.postMirrorMeterReading({ mirrorMeterReading: { mRID: this.client.generateMeterReadingMrid(), - description: 'Average Real Power (W) - Phase B', + description, lastUpdateTime, nextUpdateTime, Reading: { - value: phaseBValue.base, + value: phaseValue.base, qualityFlags: QualityFlags.Valid, }, ReadingType: { @@ -211,46 +113,36 @@ export class MirrorUsagePointDerHelper { dataQualifier: DataQualifierType.Average, flowDirection: FlowDirectionType.Reverse, intervalLength: reading.intervalSeconds, - phase: PhaseCode.PhaseB, - powerOfTenMultiplier: phaseBValue.pow10, + phase, + powerOfTenMultiplier: phaseValue.pow10, uom: UomType.W, }, }, }); - } + }; + postReading( + PhaseCode.PhaseA, + 'Average Real Power (W) - Phase A', + reading.realPowerAverage.phaseA, + ); + if (reading.realPowerAverage.phaseB) { + postReading( + PhaseCode.PhaseB, + 'Average Real Power (W) - Phase B', + reading.realPowerAverage.phaseB, + ); + } if (reading.realPowerAverage.phaseC) { - const phaseCValue = convertNumberToBaseAndPow10Exponent( + postReading( + PhaseCode.PhaseC, + 'Average Real Power (W) - Phase C', reading.realPowerAverage.phaseC, ); - - void this.postMirrorMeterReading({ - mirrorMeterReading: { - mRID: this.client.generateMeterReadingMrid(), - description: 'Average Real Power (W) - Phase C', - lastUpdateTime, - nextUpdateTime, - Reading: { - value: phaseCValue.base, - qualityFlags: QualityFlags.Valid, - }, - ReadingType: { - commodity: - CommodityType.ElectricitySecondaryMeteredValue, - kind: KindType.Power, - dataQualifier: DataQualifierType.Average, - flowDirection: FlowDirectionType.Reverse, - intervalLength: reading.intervalSeconds, - phase: PhaseCode.PhaseC, - powerOfTenMultiplier: phaseCValue.pow10, - uom: UomType.W, - }, - }, - }); } } - private postReactivePowerAverage({ + protected postReactivePowerAverage({ reading, lastUpdateTime, nextUpdateTime, @@ -287,7 +179,7 @@ export class MirrorUsagePointDerHelper { }); } - private postVoltageAverage({ + protected postVoltageAverage({ reading, lastUpdateTime, nextUpdateTime, @@ -296,46 +188,21 @@ export class MirrorUsagePointDerHelper { lastUpdateTime: Date; nextUpdateTime: Date; }) { - const phaseAValue = convertNumberToBaseAndPow10Exponent( - reading.voltageAverage.phaseA, - ); - - void this.postMirrorMeterReading({ - mirrorMeterReading: { - mRID: this.client.generateMeterReadingMrid(), - description: 'Average Voltage (V) - Phase A', - lastUpdateTime, - nextUpdateTime, - Reading: { - value: phaseAValue.base, - qualityFlags: QualityFlags.Valid, - }, - ReadingType: { - commodity: CommodityType.ElectricitySecondaryMeteredValue, - kind: KindType.Power, - dataQualifier: DataQualifierType.Average, - flowDirection: FlowDirectionType.Reverse, - intervalLength: reading.intervalSeconds, - phase: PhaseCode.PhaseA, - powerOfTenMultiplier: phaseAValue.pow10, - uom: UomType.Voltage, - }, - }, - }); - - if (reading.voltageAverage.phaseB) { - const phaseBValue = convertNumberToBaseAndPow10Exponent( - reading.voltageAverage.phaseB, - ); + const postReading = ( + phase: PhaseCode, + description: string, + value: number, + ) => { + const phaseValue = convertNumberToBaseAndPow10Exponent(value); void this.postMirrorMeterReading({ mirrorMeterReading: { mRID: this.client.generateMeterReadingMrid(), - description: 'Average Voltage (V) - Phase B', + description, lastUpdateTime, nextUpdateTime, Reading: { - value: phaseBValue.base, + value: phaseValue.base, qualityFlags: QualityFlags.Valid, }, ReadingType: { @@ -345,46 +212,36 @@ export class MirrorUsagePointDerHelper { dataQualifier: DataQualifierType.Average, flowDirection: FlowDirectionType.Reverse, intervalLength: reading.intervalSeconds, - phase: PhaseCode.PhaseB, - powerOfTenMultiplier: phaseBValue.pow10, + phase, + powerOfTenMultiplier: phaseValue.pow10, uom: UomType.Voltage, }, }, }); - } + }; + postReading( + PhaseCode.PhaseA, + 'Average Voltage (V) - Phase A', + reading.voltageAverage.phaseA, + ); + if (reading.voltageAverage.phaseB) { + postReading( + PhaseCode.PhaseB, + 'Average Voltage (V) - Phase B', + reading.voltageAverage.phaseB, + ); + } if (reading.voltageAverage.phaseC) { - const phaseCValue = convertNumberToBaseAndPow10Exponent( + postReading( + PhaseCode.PhaseC, + 'Average Voltage (V) - Phase C', reading.voltageAverage.phaseC, ); - - void this.postMirrorMeterReading({ - mirrorMeterReading: { - mRID: this.client.generateMeterReadingMrid(), - description: 'Average Voltage (V) - Phase C', - lastUpdateTime, - nextUpdateTime, - Reading: { - value: phaseCValue.base, - qualityFlags: QualityFlags.Valid, - }, - ReadingType: { - commodity: - CommodityType.ElectricitySecondaryMeteredValue, - kind: KindType.Power, - dataQualifier: DataQualifierType.Average, - flowDirection: FlowDirectionType.Reverse, - intervalLength: reading.intervalSeconds, - phase: PhaseCode.PhaseC, - powerOfTenMultiplier: phaseCValue.pow10, - uom: UomType.Voltage, - }, - }, - }); } } - private postFrequency({ + protected postFrequency({ reading, lastUpdateTime, nextUpdateTime, @@ -446,87 +303,4 @@ export class MirrorUsagePointDerHelper { }, }); } - - private getReadingFromSamples(samples: DerMonitoringSimple[]): DerReading { - return { - intervalSeconds: getSamplesIntervalSeconds(samples), - realPowerAverage: { - phaseA: averageNumbersArray( - samples.map((s) => s.der.realPower.phaseA), - ), - phaseB: averageNumbersNullableArray( - samples.map((s) => s.der.realPower.phaseB), - ), - phaseC: averageNumbersNullableArray( - samples.map((s) => s.der.realPower.phaseC), - ), - }, - reactivePowerAverage: averageNumbersArray( - samples.map((s) => s.der.reactivePower), - ), - voltageAverage: { - phaseA: averageNumbersArray( - samples.map((s) => s.der.voltage.phaseA), - ), - phaseB: averageNumbersNullableArray( - samples.map((s) => s.der.voltage.phaseB), - ), - phaseC: averageNumbersNullableArray( - samples.map((s) => s.der.voltage.phaseC), - ), - }, - frequency: { - maximum: Math.max(...samples.map((s) => s.der.frequency)), - minimum: Math.min(...samples.map((s) => s.der.frequency)), - }, - }; - } - - private getSamplesAndClear() { - const cache = this.samples; - this.samples = []; - return cache; - } - - private async postMirrorUsagePoint({ - mirrorUsagePoint, - }: { - mirrorUsagePoint: MirrorUsagePoint; - }) { - if (!this.mirrorUsagePointListHref) { - throw new Error('Missing mirrorUsagePointHref'); - } - - const data = generateMirrorUsagePointResponse(mirrorUsagePoint); - const xml = objectToXml(data); - - const response = await this.client.post( - this.mirrorUsagePointListHref, - xml, - ); - - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument - const responseXml = await parseStringPromise(response.data); - - return parseMirrorUsagePointXmlObject(responseXml); - } - - private async postMirrorMeterReading({ - mirrorMeterReading, - }: { - mirrorMeterReading: MirrorMeterReading; - }) { - if (!this.mirrorUsagePoint) { - throw new Error('Missing mirrorUsagePoint'); - } - - if (!this.mirrorUsagePoint.href) { - throw new Error('Missing mirrorUsagePoint href'); - } - - const data = generateMirrorMeterReadingResponse(mirrorMeterReading); - const xml = objectToXml(data); - - await this.client.post(this.mirrorUsagePoint.href, xml); - } } diff --git a/src/sep2/helpers/mirrorUsagePointSite.ts b/src/sep2/helpers/mirrorUsagePointSite.ts index a5ad97e..7401b99 100644 --- a/src/sep2/helpers/mirrorUsagePointSite.ts +++ b/src/sep2/helpers/mirrorUsagePointSite.ts @@ -1,19 +1,4 @@ -import { parseStringPromise } from 'xml2js'; -import { defaultPollPushRates, type SEP2Client } from '../client'; -import { - generateMirrorUsagePointResponse, - MirrorUsagePointStatus, - parseMirrorUsagePointXmlObject, - type MirrorUsagePoint, -} from '../models/mirrorUsagePoint'; import { RoleFlagsType } from '../models/roleFlagsType'; -import { objectToXml } from './xml'; -import { ServiceKind } from '../models/serviceKind'; -import { getMillisecondsToNextHourMinutesInterval } from '../../helpers/time'; -import { - generateMirrorMeterReadingResponse, - type MirrorMeterReading, -} from '../models/mirrorMeterReading'; import { getSamplesIntervalSeconds, type MonitoringSample, @@ -31,8 +16,10 @@ import { DataQualifierType } from '../models/dataQualifierType'; import { FlowDirectionType } from '../models/flowDirectionType'; import { PhaseCode } from '../models/phaseCode'; import { UomType } from '../models/uomType'; +import { MirrorUsagePointHelperBase } from './mirrorUsagePointBase'; +import { logger as pinoLogger } from '../../helpers/logger'; -type SiteMonitoringSimple = Pick; +type SiteMonitoringSample = Pick; type SiteReading = { intervalSeconds: number; @@ -45,123 +32,63 @@ type SiteReading = { }; }; -// TODO: refactor MirrorUsagePointSiteHelper and MirrorUsagePointSiteHelper to use a common base class with abstracts -export class MirrorUsagePointSiteHelper { - private client: SEP2Client; - private mirrorUsagePointListHref: string | null = null; - private mirrorUsagePoint: MirrorUsagePoint | null = null; - private samples: SiteMonitoringSimple[] = []; - private roleFlags: RoleFlagsType = +export class MirrorUsagePointSiteHelper extends MirrorUsagePointHelperBase< + SiteMonitoringSample, + SiteReading +> { + protected roleFlags = RoleFlagsType.isPremisesAggregationPoint | RoleFlagsType.isMirror; - private postTimer: NodeJS.Timeout | null = null; - - constructor({ client }: { client: SEP2Client }) { - this.client = client; - } - - public async updateMirrorUsagePointList({ - mirrorUsagePoints, - mirrorUsagePointListHref, - }: { - mirrorUsagePoints: MirrorUsagePoint[]; - mirrorUsagePointListHref: string; - }) { - this.mirrorUsagePointListHref = mirrorUsagePointListHref; - - // find existing relevant mirrorUsagePoint - const existingMirrorUsagePoint = mirrorUsagePoints.find((point) => { - return ( - point.deviceLFDI === this.client.lfdi && - point.status === MirrorUsagePointStatus.On && - point.roleFlags === this.roleFlags - ); - }); + protected description = 'Site measurement'; + protected logger = pinoLogger.child({ + module: 'MirrorUsagePointSiteHelper', + }); - if (existingMirrorUsagePoint) { - this.mirrorUsagePoint = existingMirrorUsagePoint; - - this.startPosting(); - return; - } - - // if does not exist, create new mirrorUsagePoint - this.mirrorUsagePoint = await this.postMirrorUsagePoint({ - mirrorUsagePoint: { - mRID: this.client.generateUsagePointMrid(this.roleFlags), - description: 'Site measurement', - roleFlags: this.roleFlags, - serviceCategoryKind: ServiceKind.Electricity, - status: MirrorUsagePointStatus.On, - deviceLFDI: this.client.lfdi, + protected getReadingFromSamples( + samples: SiteMonitoringSample[], + ): SiteReading { + return { + intervalSeconds: getSamplesIntervalSeconds(samples), + realPowerAverage: { + phaseA: averageNumbersArray( + samples.map((s) => s.site.realPower.phaseA), + ), + phaseB: averageNumbersNullableArray( + samples.map((s) => s.site.realPower.phaseB), + ), + phaseC: averageNumbersNullableArray( + samples.map((s) => s.site.realPower.phaseC), + ), }, - }); - - this.startPosting(); - } - - public addSample(sample: SiteMonitoringSimple) { - this.samples.push(sample); - } - - private startPosting() { - // if there's already a post timer, do nothing - if (this.postTimer) { - return; - } - - void this.post(); - } - - public post() { - const nextUpdateMilliseconds = getMillisecondsToNextHourMinutesInterval( - // convert seconds to minutes - (this.mirrorUsagePoint?.postRate ?? - defaultPollPushRates.mirrorUsagePointPush) / 60, - ); - - // set up next interval - this.postTimer = setTimeout(() => { - void this.post(); - }, nextUpdateMilliseconds); - - const samples = this.getSamplesAndClear(); - - if (samples.length > 0) { - const reading = this.getReadingFromSamples(samples); - - const lastUpdateTime = new Date(); - - const nextUpdateTime = new Date( - Date.now() + nextUpdateMilliseconds, - ); - - this.postRealPowerAverage({ - reading, - lastUpdateTime, - nextUpdateTime, - }); - - this.postReactivePowerAverage({ - reading, - lastUpdateTime, - nextUpdateTime, - }); - - this.postVoltageAverage({ - reading, - lastUpdateTime, - nextUpdateTime, - }); - - this.postFrequency({ - reading, - lastUpdateTime, - nextUpdateTime, - }); - } + reactivePowerAverage: { + phaseA: averageNumbersArray( + samples.map((s) => s.site.reactivePower.phaseA), + ), + phaseB: averageNumbersNullableArray( + samples.map((s) => s.site.reactivePower.phaseB), + ), + phaseC: averageNumbersNullableArray( + samples.map((s) => s.site.reactivePower.phaseC), + ), + }, + voltageAverage: { + phaseA: averageNumbersArray( + samples.map((s) => s.site.voltage.phaseA), + ), + phaseB: averageNumbersNullableArray( + samples.map((s) => s.site.voltage.phaseB), + ), + phaseC: averageNumbersNullableArray( + samples.map((s) => s.site.voltage.phaseC), + ), + }, + frequency: { + maximum: Math.max(...samples.map((s) => s.site.frequency)), + minimum: Math.min(...samples.map((s) => s.site.frequency)), + }, + }; } - private postRealPowerAverage({ + protected postRealPowerAverage({ reading, lastUpdateTime, nextUpdateTime, @@ -170,46 +97,21 @@ export class MirrorUsagePointSiteHelper { lastUpdateTime: Date; nextUpdateTime: Date; }) { - const phaseAValue = convertNumberToBaseAndPow10Exponent( - reading.realPowerAverage.phaseA, - ); - - void this.postMirrorMeterReading({ - mirrorMeterReading: { - mRID: this.client.generateMeterReadingMrid(), - description: 'Average Real Power (W) - Phase A', - lastUpdateTime, - nextUpdateTime, - Reading: { - value: phaseAValue.base, - qualityFlags: QualityFlags.Valid, - }, - ReadingType: { - commodity: CommodityType.ElectricitySecondaryMeteredValue, - kind: KindType.Power, - dataQualifier: DataQualifierType.Average, - flowDirection: FlowDirectionType.Forward, - intervalLength: reading.intervalSeconds, - phase: PhaseCode.PhaseA, - powerOfTenMultiplier: phaseAValue.pow10, - uom: UomType.W, - }, - }, - }); - - if (reading.realPowerAverage.phaseB) { - const phaseBValue = convertNumberToBaseAndPow10Exponent( - reading.realPowerAverage.phaseB, - ); + const postReading = ( + phase: PhaseCode, + description: string, + value: number, + ) => { + const phaseValue = convertNumberToBaseAndPow10Exponent(value); void this.postMirrorMeterReading({ mirrorMeterReading: { mRID: this.client.generateMeterReadingMrid(), - description: 'Average Real Power (W) - Phase B', + description, lastUpdateTime, nextUpdateTime, Reading: { - value: phaseBValue.base, + value: phaseValue.base, qualityFlags: QualityFlags.Valid, }, ReadingType: { @@ -219,46 +121,36 @@ export class MirrorUsagePointSiteHelper { dataQualifier: DataQualifierType.Average, flowDirection: FlowDirectionType.Forward, intervalLength: reading.intervalSeconds, - phase: PhaseCode.PhaseB, - powerOfTenMultiplier: phaseBValue.pow10, + phase, + powerOfTenMultiplier: phaseValue.pow10, uom: UomType.W, }, }, }); - } + }; + postReading( + PhaseCode.PhaseA, + 'Average Real Power (W) - Phase A', + reading.realPowerAverage.phaseA, + ); + if (reading.realPowerAverage.phaseB) { + postReading( + PhaseCode.PhaseB, + 'Average Real Power (W) - Phase B', + reading.realPowerAverage.phaseB, + ); + } if (reading.realPowerAverage.phaseC) { - const phaseCValue = convertNumberToBaseAndPow10Exponent( + postReading( + PhaseCode.PhaseC, + 'Average Real Power (W) - Phase C', reading.realPowerAverage.phaseC, ); - - void this.postMirrorMeterReading({ - mirrorMeterReading: { - mRID: this.client.generateMeterReadingMrid(), - description: 'Average Real Power (W) - Phase C', - lastUpdateTime, - nextUpdateTime, - Reading: { - value: phaseCValue.base, - qualityFlags: QualityFlags.Valid, - }, - ReadingType: { - commodity: - CommodityType.ElectricitySecondaryMeteredValue, - kind: KindType.Power, - dataQualifier: DataQualifierType.Average, - flowDirection: FlowDirectionType.Forward, - intervalLength: reading.intervalSeconds, - phase: PhaseCode.PhaseC, - powerOfTenMultiplier: phaseCValue.pow10, - uom: UomType.W, - }, - }, - }); } } - private postReactivePowerAverage({ + protected postReactivePowerAverage({ reading, lastUpdateTime, nextUpdateTime, @@ -267,46 +159,21 @@ export class MirrorUsagePointSiteHelper { lastUpdateTime: Date; nextUpdateTime: Date; }) { - const phaseAValue = convertNumberToBaseAndPow10Exponent( - reading.reactivePowerAverage.phaseA, - ); - - void this.postMirrorMeterReading({ - mirrorMeterReading: { - mRID: this.client.generateMeterReadingMrid(), - description: 'Average Reactive Power (VAR) - Phase A', - lastUpdateTime, - nextUpdateTime, - Reading: { - value: phaseAValue.base, - qualityFlags: QualityFlags.Valid, - }, - ReadingType: { - commodity: CommodityType.ElectricitySecondaryMeteredValue, - kind: KindType.Power, - dataQualifier: DataQualifierType.Average, - flowDirection: FlowDirectionType.Forward, - intervalLength: reading.intervalSeconds, - phase: PhaseCode.PhaseA, - powerOfTenMultiplier: phaseAValue.pow10, - uom: UomType.var, - }, - }, - }); - - if (reading.reactivePowerAverage.phaseB) { - const phaseBValue = convertNumberToBaseAndPow10Exponent( - reading.reactivePowerAverage.phaseB, - ); + const postReading = ( + phase: PhaseCode, + description: string, + value: number, + ) => { + const phaseValue = convertNumberToBaseAndPow10Exponent(value); void this.postMirrorMeterReading({ mirrorMeterReading: { mRID: this.client.generateMeterReadingMrid(), - description: 'Average Reactive Power (VAR) - Phase B', + description, lastUpdateTime, nextUpdateTime, Reading: { - value: phaseBValue.base, + value: phaseValue.base, qualityFlags: QualityFlags.Valid, }, ReadingType: { @@ -316,46 +183,36 @@ export class MirrorUsagePointSiteHelper { dataQualifier: DataQualifierType.Average, flowDirection: FlowDirectionType.Forward, intervalLength: reading.intervalSeconds, - phase: PhaseCode.PhaseB, - powerOfTenMultiplier: phaseBValue.pow10, + phase, + powerOfTenMultiplier: phaseValue.pow10, uom: UomType.var, }, }, }); - } + }; + postReading( + PhaseCode.PhaseA, + 'Average Reactive Power (VAR) - Phase A', + reading.reactivePowerAverage.phaseA, + ); + if (reading.reactivePowerAverage.phaseB) { + postReading( + PhaseCode.PhaseB, + 'Average Reactive Power (VAR) - Phase B', + reading.reactivePowerAverage.phaseB, + ); + } if (reading.reactivePowerAverage.phaseC) { - const phaseCValue = convertNumberToBaseAndPow10Exponent( + postReading( + PhaseCode.PhaseC, + 'Average Reactive Power (VAR) - Phase C', reading.reactivePowerAverage.phaseC, ); - - void this.postMirrorMeterReading({ - mirrorMeterReading: { - mRID: this.client.generateMeterReadingMrid(), - description: 'Average Reactive Power (VAR) - Phase C', - lastUpdateTime, - nextUpdateTime, - Reading: { - value: phaseCValue.base, - qualityFlags: QualityFlags.Valid, - }, - ReadingType: { - commodity: - CommodityType.ElectricitySecondaryMeteredValue, - kind: KindType.Power, - dataQualifier: DataQualifierType.Average, - flowDirection: FlowDirectionType.Forward, - intervalLength: reading.intervalSeconds, - phase: PhaseCode.PhaseC, - powerOfTenMultiplier: phaseCValue.pow10, - uom: UomType.var, - }, - }, - }); } } - private postVoltageAverage({ + protected postVoltageAverage({ reading, lastUpdateTime, nextUpdateTime, @@ -364,46 +221,21 @@ export class MirrorUsagePointSiteHelper { lastUpdateTime: Date; nextUpdateTime: Date; }) { - const phaseAValue = convertNumberToBaseAndPow10Exponent( - reading.voltageAverage.phaseA, - ); - - void this.postMirrorMeterReading({ - mirrorMeterReading: { - mRID: this.client.generateMeterReadingMrid(), - description: 'Average Voltage (V) - Phase A', - lastUpdateTime, - nextUpdateTime, - Reading: { - value: phaseAValue.base, - qualityFlags: QualityFlags.Valid, - }, - ReadingType: { - commodity: CommodityType.ElectricitySecondaryMeteredValue, - kind: KindType.Power, - dataQualifier: DataQualifierType.Average, - flowDirection: FlowDirectionType.Forward, - intervalLength: reading.intervalSeconds, - phase: PhaseCode.PhaseA, - powerOfTenMultiplier: phaseAValue.pow10, - uom: UomType.Voltage, - }, - }, - }); - - if (reading.voltageAverage.phaseB) { - const phaseBValue = convertNumberToBaseAndPow10Exponent( - reading.voltageAverage.phaseB, - ); + const postReading = ( + phase: PhaseCode, + description: string, + value: number, + ) => { + const phaseValue = convertNumberToBaseAndPow10Exponent(value); void this.postMirrorMeterReading({ mirrorMeterReading: { mRID: this.client.generateMeterReadingMrid(), - description: 'Average Voltage (V) - Phase B', + description, lastUpdateTime, nextUpdateTime, Reading: { - value: phaseBValue.base, + value: phaseValue.base, qualityFlags: QualityFlags.Valid, }, ReadingType: { @@ -413,46 +245,36 @@ export class MirrorUsagePointSiteHelper { dataQualifier: DataQualifierType.Average, flowDirection: FlowDirectionType.Forward, intervalLength: reading.intervalSeconds, - phase: PhaseCode.PhaseB, - powerOfTenMultiplier: phaseBValue.pow10, + phase, + powerOfTenMultiplier: phaseValue.pow10, uom: UomType.Voltage, }, }, }); - } + }; + postReading( + PhaseCode.PhaseA, + 'Average Voltage (V) - Phase A', + reading.voltageAverage.phaseA, + ); + if (reading.voltageAverage.phaseB) { + postReading( + PhaseCode.PhaseB, + 'Average Voltage (V) - Phase B', + reading.voltageAverage.phaseB, + ); + } if (reading.voltageAverage.phaseC) { - const phaseCValue = convertNumberToBaseAndPow10Exponent( + postReading( + PhaseCode.PhaseC, + 'Average Voltage (V) - Phase C', reading.voltageAverage.phaseC, ); - - void this.postMirrorMeterReading({ - mirrorMeterReading: { - mRID: this.client.generateMeterReadingMrid(), - description: 'Average Voltage (V) - Phase C', - lastUpdateTime, - nextUpdateTime, - Reading: { - value: phaseCValue.base, - qualityFlags: QualityFlags.Valid, - }, - ReadingType: { - commodity: - CommodityType.ElectricitySecondaryMeteredValue, - kind: KindType.Power, - dataQualifier: DataQualifierType.Average, - flowDirection: FlowDirectionType.Forward, - intervalLength: reading.intervalSeconds, - phase: PhaseCode.PhaseC, - powerOfTenMultiplier: phaseCValue.pow10, - uom: UomType.Voltage, - }, - }, - }); } } - private postFrequency({ + protected postFrequency({ reading, lastUpdateTime, nextUpdateTime, @@ -514,97 +336,4 @@ export class MirrorUsagePointSiteHelper { }, }); } - - private getReadingFromSamples( - samples: SiteMonitoringSimple[], - ): SiteReading { - return { - intervalSeconds: getSamplesIntervalSeconds(samples), - realPowerAverage: { - phaseA: averageNumbersArray( - samples.map((s) => s.site.realPower.phaseA), - ), - phaseB: averageNumbersNullableArray( - samples.map((s) => s.site.realPower.phaseB), - ), - phaseC: averageNumbersNullableArray( - samples.map((s) => s.site.realPower.phaseC), - ), - }, - reactivePowerAverage: { - phaseA: averageNumbersArray( - samples.map((s) => s.site.reactivePower.phaseA), - ), - phaseB: averageNumbersNullableArray( - samples.map((s) => s.site.reactivePower.phaseB), - ), - phaseC: averageNumbersNullableArray( - samples.map((s) => s.site.reactivePower.phaseC), - ), - }, - voltageAverage: { - phaseA: averageNumbersArray( - samples.map((s) => s.site.voltage.phaseA), - ), - phaseB: averageNumbersNullableArray( - samples.map((s) => s.site.voltage.phaseB), - ), - phaseC: averageNumbersNullableArray( - samples.map((s) => s.site.voltage.phaseC), - ), - }, - frequency: { - maximum: Math.max(...samples.map((s) => s.site.frequency)), - minimum: Math.min(...samples.map((s) => s.site.frequency)), - }, - }; - } - - private getSamplesAndClear() { - const cache = this.samples; - this.samples = []; - return cache; - } - - private async postMirrorUsagePoint({ - mirrorUsagePoint, - }: { - mirrorUsagePoint: MirrorUsagePoint; - }) { - if (!this.mirrorUsagePointListHref) { - throw new Error('Missing mirrorUsagePointHref'); - } - - const data = generateMirrorUsagePointResponse(mirrorUsagePoint); - const xml = objectToXml(data); - - const response = await this.client.post( - this.mirrorUsagePointListHref, - xml, - ); - - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument - const responseXml = await parseStringPromise(response.data); - - return parseMirrorUsagePointXmlObject(responseXml); - } - - private async postMirrorMeterReading({ - mirrorMeterReading, - }: { - mirrorMeterReading: MirrorMeterReading; - }) { - if (!this.mirrorUsagePoint) { - throw new Error('Missing mirrorUsagePoint'); - } - - if (!this.mirrorUsagePoint.href) { - throw new Error('Missing mirrorUsagePoint href'); - } - - const data = generateMirrorMeterReadingResponse(mirrorMeterReading); - const xml = objectToXml(data); - - await this.client.post(this.mirrorUsagePoint.href, xml); - } }