diff --git a/config.schema.json b/config.schema.json index ca22c79..4d76964 100644 --- a/config.schema.json +++ b/config.schema.json @@ -262,6 +262,32 @@ ], "additionalProperties": false, "description": "SMA inverter configuration" + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "goodwe" + }, + "model": { + "type": "string", + "const": "et" + }, + "connection": { + "$ref": "#/definitions/config/properties/inverters/items/anyOf/0/properties/connection" + }, + "unitId": { + "$ref": "#/definitions/config/properties/inverters/items/anyOf/0/properties/unitId" + } + }, + "required": [ + "type", + "model", + "connection" + ], + "additionalProperties": false, + "description": "Goodwe inverter configuration" } ] }, @@ -349,6 +375,32 @@ "additionalProperties": false, "description": "SMA meter configuration" }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "goodwe" + }, + "model": { + "type": "string", + "const": "et" + }, + "connection": { + "$ref": "#/definitions/config/properties/inverters/items/anyOf/0/properties/connection" + }, + "unitId": { + "$ref": "#/definitions/config/properties/inverters/items/anyOf/0/properties/unitId" + } + }, + "required": [ + "type", + "model", + "connection" + ], + "additionalProperties": false, + "description": "Goodwe meter configuration" + }, { "type": "object", "properties": { diff --git a/src/connections/goodwe/et.ts b/src/connections/goodwe/et.ts new file mode 100644 index 0000000..d656332 --- /dev/null +++ b/src/connections/goodwe/et.ts @@ -0,0 +1,92 @@ +import { type ModbusConnection } from '../modbus/connection/base.js'; +import { type Logger } from 'pino'; +import { getModbusConnection } from '../modbus/connections.js'; +import { type ModbusSchema } from '../../helpers/config.js'; +import { + GoodweEtDeviceParametersModel, + type GoodweEtDeviceParameters, +} from './models/et/deviceParameters.js'; +import { type GoodweEtInverterRunningData1 } from './models/et/inverterRunningData1.js'; +import { GoodweEtInverterRunningData1Model } from './models/et/inverterRunningData1.js'; +import { + GoodweEtMeterDataModel, + type GoodweEtMeterData, +} from './models/et/meterData.js'; +import { writeLatency } from '../../helpers/influxdb.js'; + +export class GoodweEtConnection { + protected readonly modbusConnection: ModbusConnection; + protected readonly unitId: number; + private logger: Logger; + + // the device parameters model should never change so we can cache it + private deviceParameters: GoodweEtDeviceParameters | null = null; + + constructor({ connection, unitId }: ModbusSchema) { + this.modbusConnection = getModbusConnection(connection); + this.unitId = unitId; + this.logger = this.modbusConnection.logger.child({ + module: 'GoodweConnection', + unitId, + }); + } + + async getDeviceParameters(): Promise { + if (this.deviceParameters) { + return this.deviceParameters; + } + + const data = await GoodweEtDeviceParametersModel.read({ + modbusConnection: this.modbusConnection, + address: { + start: 35001, + length: 15, + }, + unitId: this.unitId, + }); + + this.deviceParameters = data; + + return data; + } + + async getInverterRunningData1(): Promise { + const data = await GoodweEtInverterRunningData1Model.read({ + modbusConnection: this.modbusConnection, + address: { + start: 35121, + length: 24, + }, + unitId: this.unitId, + }); + + return data; + } + + async getMeterData(): Promise { + const start = performance.now(); + + const data = await GoodweEtMeterDataModel.read({ + modbusConnection: this.modbusConnection, + address: { + start: 36003, + length: 55, + }, + unitId: this.unitId, + }); + + writeLatency({ + field: 'GoodweEtConnection', + duration: performance.now() - start, + tags: { + model: 'GoodweEtMeterDataModel', + }, + }); + + return data; + } + + public onDestroy(): void { + this.modbusConnection.close(); + } +} diff --git a/src/connections/goodwe/models/et/deviceParameters.ts b/src/connections/goodwe/models/et/deviceParameters.ts new file mode 100644 index 0000000..aead48d --- /dev/null +++ b/src/connections/goodwe/models/et/deviceParameters.ts @@ -0,0 +1,36 @@ +import { modbusModelFactory } from '../../../modbus/modbusModelFactory.js'; +import { + registersToString, + registersToUint16, +} from '../../../modbus/helpers/converters.js'; + +export type GoodweEtDeviceParameters = { + // Inverter rated power + RatePower: number; + // Inverter serial number. ASCII,16 bytes,readtogether, include OEMproducts. + INVSN: string; + // ASCII,10 bytes + Modelname: string; +}; + +export const GoodweEtDeviceParametersModel = + modbusModelFactory({ + name: 'GoodweEtDeviceParametersModel', + mapping: { + RatePower: { + start: 0, + end: 1, + readConverter: registersToUint16, + }, + INVSN: { + start: 2, + end: 11, + readConverter: registersToString, + }, + Modelname: { + start: 12, + end: 22, + readConverter: registersToString, + }, + }, + }); diff --git a/src/connections/goodwe/models/et/inverterRunningData1.ts b/src/connections/goodwe/models/et/inverterRunningData1.ts new file mode 100644 index 0000000..65d4a4e --- /dev/null +++ b/src/connections/goodwe/models/et/inverterRunningData1.ts @@ -0,0 +1,143 @@ +import { modbusModelFactory } from '../../../modbus/modbusModelFactory.js'; +import { + registersToInt32, + registersToUint16, +} from '../../../modbus/helpers/converters.js'; + +export type GoodweEtInverterRunningData1 = { + // R phase Grid voltage + Vgrid_R: number; + // R phase Grid current + Igrid_R: number; + // R phase Grid Frequency + Fgrid_R: number; + // R phase Grid Power(Inv power) + Pgrid_R: number; + // S phase Grid voltage + Vgrid_S: number; + // S phase Grid current + Igrid_S: number; + // S phase Grid Frequency + Fgrid_S: number; + // S phase Grid Power(Inv power) + Pgrid_S: number; + // T phase Grid voltage + Vgrid_T: number; + // T phase Grid current + Igrid_T: number; + // T phase Grid Frequency + Fgrid_T: number; + // T phase Grid Power(Inv power) + Pgrid_T: number; + // Grid connection status + GridMode: GridMode; + // Total Power of Inverter(Inv power) + TotalINVPower: number; + // Total Active Power Of Inverter. (If meter connection ok, it is meter power.If meter connection fail, it is inverter on-grid port power) + ACActivePower: number; + // Total Reactive Power Of Inverter + ACReactivePower: number; + // Total Apparent Power Of Inverter + ACApparentPower: number; +}; + +export enum GridMode { + // 0x00 Loss, inverter disconnects to Grid + Loss = 0, + // 0x01 OK, inverter connects to Grid + OK = 1, + // 0x02 Fault, something is wrong + Fault = 2, +} + +export const GoodweEtInverterRunningData1Model = + modbusModelFactory({ + name: 'GoodweEtInverterRunningData1Model', + mapping: { + Vgrid_R: { + start: 0, + end: 1, + readConverter: (value) => registersToUint16(value, -1), + }, + Igrid_R: { + start: 1, + end: 2, + readConverter: (value) => registersToUint16(value, -1), + }, + Fgrid_R: { + start: 2, + end: 3, + readConverter: (value) => registersToUint16(value, -2), + }, + Pgrid_R: { + start: 3, + end: 5, + readConverter: registersToInt32, + }, + Vgrid_S: { + start: 5, + end: 6, + readConverter: (value) => registersToUint16(value, -1), + }, + Igrid_S: { + start: 6, + end: 7, + readConverter: (value) => registersToUint16(value, -1), + }, + Fgrid_S: { + start: 7, + end: 8, + readConverter: (value) => registersToUint16(value, -2), + }, + Pgrid_S: { + start: 8, + end: 10, + readConverter: registersToInt32, + }, + Vgrid_T: { + start: 10, + end: 11, + readConverter: (value) => registersToUint16(value, -1), + }, + Igrid_T: { + start: 11, + end: 12, + readConverter: (value) => registersToUint16(value, -1), + }, + Fgrid_T: { + start: 12, + end: 13, + readConverter: (value) => registersToUint16(value, -2), + }, + Pgrid_T: { + start: 13, + end: 15, + readConverter: registersToInt32, + }, + GridMode: { + start: 15, + end: 16, + readConverter: registersToUint16, + }, + TotalINVPower: { + start: 16, + end: 18, + readConverter: registersToInt32, + }, + ACActivePower: { + start: 18, + end: 20, + readConverter: registersToInt32, + }, + ACReactivePower: { + start: 20, + end: 22, + readConverter: registersToInt32, + }, + ACApparentPower: { + start: 22, + end: 24, + readConverter: registersToInt32, + }, + }, + }); diff --git a/src/connections/goodwe/models/et/meterData.ts b/src/connections/goodwe/models/et/meterData.ts new file mode 100644 index 0000000..834a468 --- /dev/null +++ b/src/connections/goodwe/models/et/meterData.ts @@ -0,0 +1,287 @@ +import { modbusModelFactory } from '../../../modbus/modbusModelFactory.js'; +import { + registersToUint32, + registersToInt32, + registersToInt16, + registersToUint16, + registersToUint16Nullable, +} from '../../../modbus/helpers/converters.js'; + +export type GoodweEtMeterData = { + // 1: connect correctly, + // 2: connect reverse(CT) + // 4:connect incorrectly + // For example: 0X0124 means Phase R connect incorrectly,Phase S connect reverse, Phase T connect correctly + bMeterConnectStatus: number; + // 1: OK, 0: NG + MeterCommStatus: number; + // PMeter R + // If ARM Version>9,please refer to 36019~36041 + MeterActivepowerR: number; + // PMeter S + // If ARM Version>9,please refer to 36019~36041 + MeterActivepowerS: number; + // PMeter T + // If ARM Version>9,please refer to 36019~36041 + MeterActivepowerT: number; + // Pmeter + // If ARM Version>9,please refer to 36019~36041. If three phase meter, it is total power + MeterTotalActivepower: number; + // Pmeter reactive power + // If ARM Version>9,please refer to 36019~36041 + MeterTotalReactivepower: number; + // Meter power factor R + MeterPF_R: number; + // Meter power factor S + MeterPF_S: number; + // Meter power factor T + MeterPF_T: number; + // Meter power factor + MeterPowerFactor: number; + MeterFrequence: number; + // Total Feed Energy To Grid. Read frommeter + E_Total_Sell: number; + // Total Energy From Grid. Read frommeter + E_Total_Buy: number; + // ARM>09 Pmeter R + MeterActivepowerR2: number; + // ARM>09 Pmeter S + MeterActivepowerS2: number; + // ARM>09 Pmeter T + MeterActivepowerT2: number; + // ARM>09 Pmeter + MeterTotalActivepower2: number; + // Pmeter R Reactive Power + MeterReactivepowerR: number; + // Pmeter S Reactive Power + MeterReactivepowerS: number; + // Pmeter T Reactive Power + MeterReactivepowerT: number; + // Pmeter Reactive Power + MeterTotalReactivepower2: number; + // Pmeter R Apparent Power + MeterApparentpowerR: number; + // Pmeter S Apparent Power + MeterApparentpowerS: number; + // Pmeter T Apparent Power + MeterApparentpowerT: number; + // Pmeter Apparent Power + MeterTotalApparentpower: number; + // Only for GoodWe Smart Meter (0: Singlephase, 1: 3P3W, 2: 3P4W, 3: HomeKit, 4: + MeterType: number; + // Only for GGoMod1W00e0SDm) art Meter + MeterSoftwareVersion: number; + // Only for AC Couple inverter. Detect PVinverter + MeterCT2Activepower: number; + CT2_E_Total_sell: number; + CT2_E_Total_buy: number; + MeterCT2status: number; + // Phase R voltage frommeter + meterVoltageR: number | null; + // Phase S voltage frommeter + meterVoltageS: number | null; + // Phase T voltage frommeter + meterVoltageT: number | null; + // Phase R current frommeter + meterCurrentR: number; + // Phase S current frommeter + meterCurrentS: number; + // Phase T current frommeter + meterCurrentT: number; +}; + +export const GoodweEtMeterDataModel = modbusModelFactory({ + name: 'GoodweEtMeterDataModel', + mapping: { + bMeterConnectStatus: { + start: 0, // 36003 - 36003 + end: 1, + readConverter: registersToUint16, + }, + MeterCommStatus: { + start: 1, // 36004 - 36003 + end: 2, + readConverter: registersToUint16, + }, + MeterActivepowerR: { + start: 2, // 36005 - 36003 + end: 3, + readConverter: registersToInt16, + }, + MeterActivepowerS: { + start: 3, // 36006 - 36003 + end: 4, + readConverter: registersToInt16, + }, + MeterActivepowerT: { + start: 4, // 36007 - 36003 + end: 5, + readConverter: registersToInt16, + }, + MeterTotalActivepower: { + start: 5, // 36008 - 36003 + end: 6, + readConverter: registersToInt16, + }, + MeterTotalReactivepower: { + start: 6, // 36009 - 36003 + end: 7, + readConverter: registersToInt16, + }, + MeterPF_R: { + start: 7, // 36010 - 36003 + end: 8, + readConverter: (value) => registersToInt16(value, -2), // /100 + }, + MeterPF_S: { + start: 8, // 36011 - 36003 + end: 9, + readConverter: (value) => registersToInt16(value, -2), + }, + MeterPF_T: { + start: 9, // 36012 - 36003 + end: 10, + readConverter: (value) => registersToInt16(value, -2), + }, + MeterPowerFactor: { + start: 10, // 36013 - 36003 + end: 11, + readConverter: (value) => registersToInt16(value, -2), + }, + MeterFrequence: { + start: 11, // 36014 - 36003 + end: 12, + readConverter: (value) => registersToUint16(value, -2), + }, + E_Total_Sell: { + start: 12, // 36015 - 36003 + end: 14, + readConverter: registersToUint32, + }, + E_Total_Buy: { + start: 14, // 36017 - 36003 + end: 16, + readConverter: registersToUint32, + }, + MeterActivepowerR2: { + start: 16, // 36019 - 36003 + end: 18, + readConverter: registersToInt32, + }, + MeterActivepowerS2: { + start: 18, // 36021 - 36003 + end: 20, + readConverter: registersToInt32, + }, + MeterActivepowerT2: { + start: 20, // 36023 - 36003 + end: 22, + readConverter: registersToInt32, + }, + MeterTotalActivepower2: { + start: 22, // 36025 - 36003 + end: 24, + readConverter: registersToInt32, + }, + MeterReactivepowerR: { + start: 24, // 36027 - 36003 + end: 26, + readConverter: registersToInt32, + }, + MeterReactivepowerS: { + start: 26, // 36029 - 36003 + end: 28, + readConverter: registersToInt32, + }, + MeterReactivepowerT: { + start: 28, // 36031 - 36003 + end: 30, + readConverter: registersToInt32, + }, + MeterTotalReactivepower2: { + start: 30, // 36033 - 36003 + end: 32, + readConverter: registersToInt32, + }, + MeterApparentpowerR: { + start: 32, // 36035 - 36003 + end: 34, + readConverter: registersToInt32, + }, + MeterApparentpowerS: { + start: 34, // 36037 - 36003 + end: 36, + readConverter: registersToInt32, + }, + MeterApparentpowerT: { + start: 36, // 36039 - 36003 + end: 38, + readConverter: registersToInt32, + }, + MeterTotalApparentpower: { + start: 38, // 36041 - 36003 + end: 40, + readConverter: registersToInt32, + }, + MeterType: { + start: 40, // 36043 - 36003 + end: 41, + readConverter: registersToUint16, + }, + MeterSoftwareVersion: { + start: 41, // 36044 - 36003 + end: 42, + readConverter: registersToUint16, + }, + MeterCT2Activepower: { + start: 42, // 36045 - 36003 + end: 44, + readConverter: registersToInt32, + }, + CT2_E_Total_sell: { + start: 44, // 36047 - 36003 + end: 46, + readConverter: (value) => registersToUint32(value, -2), // /100 + }, + CT2_E_Total_buy: { + start: 46, // 36049 - 36003 + end: 48, + readConverter: (value) => registersToUint32(value, -2), // /100 + }, + MeterCT2status: { + start: 48, // 36051 - 36003 + end: 49, + readConverter: registersToUint16, + }, + meterVoltageR: { + start: 49, // 36052 - 36003 + end: 50, + readConverter: (value) => registersToUint16Nullable(value, -1), // /10 + }, + meterVoltageS: { + start: 50, // 36053 - 36003 + end: 51, + readConverter: (value) => registersToUint16Nullable(value, -1), // /10 + }, + meterVoltageT: { + start: 51, // 36054 - 36003 + end: 52, + readConverter: (value) => registersToUint16Nullable(value, -1), // /10 + }, + meterCurrentR: { + start: 52, // 36055 - 36003 + end: 53, + readConverter: (value) => registersToUint16(value, -1), // /10 + }, + meterCurrentS: { + start: 53, // 36056 - 36003 + end: 54, + readConverter: (value) => registersToUint16(value, -1), // /10 + }, + meterCurrentT: { + start: 54, // 36057 - 36003 + end: 55, + readConverter: (value) => registersToUint16(value, -1), // /10 + }, + }, +}); diff --git a/src/coordinator/helpers/derSample.ts b/src/coordinator/helpers/derSample.ts index 03f39de..6e59590 100644 --- a/src/coordinator/helpers/derSample.ts +++ b/src/coordinator/helpers/derSample.ts @@ -18,6 +18,11 @@ import { type ConnectStatusValue } from '../../sep2/models/connectStatus.js'; // aligns with the CSIP-AUS requirements for DER monitoring export const derSampleDataSchema = z.object({ + /** + * Positive values = DER export power + * + * Negative values = DER import power + */ realPower: z.union([ perPhaseNetMeasurementSchema, noPhaseMeasurementSchema, diff --git a/src/coordinator/helpers/inverterSample.ts b/src/coordinator/helpers/inverterSample.ts index a123782..e11d2ff 100644 --- a/src/coordinator/helpers/inverterSample.ts +++ b/src/coordinator/helpers/inverterSample.ts @@ -10,6 +10,7 @@ import { SunSpecInverterDataPoller } from '../../inverter/sunspec/index.js'; import { type InverterConfiguration } from './inverterController.js'; import { type Logger } from 'pino'; import { SmaInverterDataPoller } from '../../inverter/sma/index.js'; +import { GoodweEtInverterDataPoller } from '../../inverter/goodwe/et.js'; export class InvertersPoller extends EventEmitter<{ data: [DerSample]; @@ -46,6 +47,19 @@ export class InvertersPoller extends EventEmitter<{ inverterIndex: index, }).on('data', inverterOnData); } + case 'goodwe': { + switch (inverterConfig.model) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + case 'et': { + return new GoodweEtInverterDataPoller({ + goodweEtInverterConfig: inverterConfig, + applyControl: + config.inverterControl.enabled, + inverterIndex: index, + }).on('data', inverterOnData); + } + } + } } }, ); diff --git a/src/coordinator/helpers/siteSample.ts b/src/coordinator/helpers/siteSample.ts index d70bfa9..7de2aa3 100644 --- a/src/coordinator/helpers/siteSample.ts +++ b/src/coordinator/helpers/siteSample.ts @@ -5,6 +5,7 @@ import { SunSpecMeterSiteSamplePoller } from '../../meters/sunspec/index.js'; import { type SiteSamplePollerBase } from '../../meters/siteSamplePollerBase.js'; import { type InvertersPoller } from './inverterSample.js'; import { SmaMeterSiteSamplePoller } from '../../meters/sma/index.js'; +import { GoodweEtSiteSamplePoller } from '../../meters/goodwe/et.js'; export function getSiteSamplePollerInstance({ config, @@ -14,6 +15,17 @@ export function getSiteSamplePollerInstance({ invertersPoller: InvertersPoller; }): SiteSamplePollerBase { switch (config.meter.type) { + case 'goodwe': { + switch (config.meter.model) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + case 'et': { + return new GoodweEtSiteSamplePoller({ + goodweEtConfig: config.meter, + }); + } + } + break; + } case 'sunspec': { return new SunSpecMeterSiteSamplePoller({ sunspecMeterConfig: config.meter, diff --git a/src/helpers/config.ts b/src/helpers/config.ts index fd80655..4875421 100644 --- a/src/helpers/config.ts +++ b/src/helpers/config.ts @@ -156,6 +156,13 @@ export const configSchema = z.object({ }) .merge(modbusSchema) .describe('SMA inverter configuration'), + z + .object({ + type: z.literal('goodwe'), + model: z.literal('et'), + }) + .merge(modbusSchema) + .describe('Goodwe inverter configuration'), ]), ) .describe('Inverter configuration'), @@ -196,6 +203,13 @@ A longer time will smooth out load changes but may result in overshoot.`, }) .merge(modbusSchema) .describe('SMA meter configuration'), + z + .object({ + type: z.literal('goodwe'), + model: z.literal('et'), + }) + .merge(modbusSchema) + .describe('Goodwe meter configuration'), z .object({ type: z.literal('powerwall2'), diff --git a/src/inverter/goodwe/et.ts b/src/inverter/goodwe/et.ts new file mode 100644 index 0000000..c2bb270 --- /dev/null +++ b/src/inverter/goodwe/et.ts @@ -0,0 +1,161 @@ +import { type InverterData } from '../inverterData.js'; +import { ConnectStatusValue } from '../../sep2/models/connectStatus.js'; +import { OperationalModeStatusValue } from '../../sep2/models/operationModeStatus.js'; +import { InverterDataPollerBase } from '../inverterDataPollerBase.js'; +import { type InverterConfiguration } from '../../coordinator/helpers/inverterController.js'; +import { type Config } from '../../helpers/config.js'; +import { writeLatency } from '../../helpers/influxdb.js'; +import { averageNumbersNullableArray } from '../../helpers/number.js'; +import { DERTyp } from '../../connections/sunspec/models/nameplate.js'; +import { GoodweEtConnection } from '../../connections/goodwe/et.js'; +import { type GoodweEtDeviceParameters } from '../../connections/goodwe/models/et/deviceParameters.js'; +import { + GridMode, + type GoodweEtInverterRunningData1, +} from '../../connections/goodwe/models/et/inverterRunningData1.js'; + +export class GoodweEtInverterDataPoller extends InverterDataPollerBase { + private connection: GoodweEtConnection; + + constructor({ + goodweEtInverterConfig, + inverterIndex, + applyControl, + }: { + goodweEtInverterConfig: Extract< + Config['inverters'][number], + { type: 'goodwe'; model: 'et' } + >; + inverterIndex: number; + applyControl: boolean; + }) { + super({ + name: 'GoodweEtInverterDataPoller', + pollingIntervalMs: 200, + applyControl, + inverterIndex, + }); + + this.connection = new GoodweEtConnection(goodweEtInverterConfig); + + void this.startPolling(); + } + + override async getInverterData(): Promise { + const start = performance.now(); + + const deviceParameters = await this.connection.getDeviceParameters(); + + writeLatency({ + field: 'GoodweEtInverterDataPoller', + duration: performance.now() - start, + tags: { + inverterIndex: this.inverterIndex.toString(), + model: 'deviceParameters', + }, + }); + + const inverterRunningData1 = + await this.connection.getInverterRunningData1(); + + writeLatency({ + field: 'GoodweEtInverterDataPoller', + duration: performance.now() - start, + tags: { + inverterIndex: this.inverterIndex.toString(), + model: 'inverterRunningData1', + }, + }); + + const models: InverterModels = { + deviceParameters, + inverterRunningData1, + }; + + const end = performance.now(); + const duration = end - start; + + this.logger.trace({ duration, models }, 'Got inverter data'); + + const inverterData = generateInverterData(models); + + return inverterData; + } + + override onDestroy(): void { + this.connection.onDestroy(); + } + + // eslint-disable-next-line @typescript-eslint/require-await + override async onControl( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _inverterConfiguration: InverterConfiguration, + ): Promise { + throw new Error('Method not implemented.'); + } +} + +type InverterModels = { + deviceParameters: GoodweEtDeviceParameters; + inverterRunningData1: GoodweEtInverterRunningData1; +}; + +export function generateInverterData({ + deviceParameters, + inverterRunningData1, +}: InverterModels): InverterData { + return { + date: new Date(), + inverter: { + realPower: inverterRunningData1.TotalINVPower, + reactivePower: inverterRunningData1.ACReactivePower, + voltagePhaseA: inverterRunningData1.Vgrid_R, + voltagePhaseB: inverterRunningData1.Vgrid_S, + voltagePhaseC: inverterRunningData1.Vgrid_T, + frequency: averageNumbersNullableArray([ + inverterRunningData1.Fgrid_R || null, + inverterRunningData1.Fgrid_S || null, + inverterRunningData1.Fgrid_T || null, + ])!, + }, + nameplate: { + type: DERTyp.PV_STOR, + maxW: deviceParameters.RatePower, + maxVA: deviceParameters.RatePower, + maxVar: deviceParameters.RatePower, + }, + settings: { + maxW: deviceParameters.RatePower, + maxVA: deviceParameters.RatePower, + maxVar: deviceParameters.RatePower, + }, + status: generateInverterDataStatus({ inverterRunningData1 }), + }; +} + +export function generateInverterDataStatus({ + inverterRunningData1, +}: { + inverterRunningData1: GoodweEtInverterRunningData1; +}): InverterData['status'] { + return { + operationalModeStatus: + inverterRunningData1.GridMode === GridMode.OK + ? OperationalModeStatusValue.OperationalMode + : OperationalModeStatusValue.Off, + genConnectStatus: (() => { + switch (inverterRunningData1.GridMode) { + case GridMode.OK: + return ( + ConnectStatusValue.Connected | + ConnectStatusValue.Available | + ConnectStatusValue.Operating + ); + case GridMode.Loss: + return 0 as ConnectStatusValue; + case GridMode.Fault: + return ConnectStatusValue.Fault; + } + })(), + }; +} diff --git a/src/meters/goodwe/et.ts b/src/meters/goodwe/et.ts new file mode 100644 index 0000000..37d91d2 --- /dev/null +++ b/src/meters/goodwe/et.ts @@ -0,0 +1,84 @@ +import { type Config } from '../../helpers/config.js'; +import { GoodweEtConnection } from '../../connections/goodwe/et.js'; +import { SiteSamplePollerBase } from '../../meters/siteSamplePollerBase.js'; +import { type GoodweEtMeterData } from '../../connections/goodwe/models/et/meterData.js'; +import { type SiteSample } from '../siteSample.js'; + +export class GoodweEtSiteSamplePoller extends SiteSamplePollerBase { + private connection: GoodweEtConnection; + + constructor({ + goodweEtConfig, + }: { + goodweEtConfig: Extract< + Config['meter'], + { type: 'goodwe'; model: 'et' } + >; + }) { + super({ + name: 'GoodweEtSiteSamplePoller', + pollingIntervalMs: 200, + }); + + this.connection = new GoodweEtConnection(goodweEtConfig); + + void this.startPolling(); + } + + override async getSiteSample(): Promise { + const start = performance.now(); + + const meterData = await this.connection.getMeterData(); + + const end = performance.now(); + const duration = end - start; + + this.logger.trace({ duration, meterData }, 'got meter data'); + + const siteSample = generateSiteSample({ meterData }); + + return siteSample; + } + + override onDestroy(): void { + this.connection.onDestroy(); + } +} + +function generateSiteSample({ + meterData, +}: { + meterData: GoodweEtMeterData; +}): SiteSample { + return { + date: new Date(), + realPower: { + type: 'perPhaseNet', + phaseA: + -meterData.MeterActivepowerR2 || -meterData.MeterActivepowerR, + phaseB: + -meterData.MeterActivepowerS2 || -meterData.MeterActivepowerS, + phaseC: + -meterData.MeterActivepowerT2 || -meterData.MeterActivepowerT, + net: + -meterData.MeterTotalActivepower2 || + -meterData.MeterTotalActivepower, + }, + reactivePower: { + type: 'perPhaseNet', + phaseA: -meterData.MeterReactivepowerR, + phaseB: -meterData.MeterReactivepowerS, + phaseC: -meterData.MeterReactivepowerT, + net: + -meterData.MeterTotalReactivepower2 || + -meterData.MeterTotalReactivepower, + }, + voltage: { + type: 'perPhase', + phaseA: meterData.meterVoltageR, + phaseB: meterData.meterVoltageS, + phaseC: meterData.meterVoltageT, + }, + frequency: meterData.MeterFrequence, + }; +}