diff --git a/README.md b/README.md index 9dbfd889..591c5fab 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Build Status](https://travis-ci.org/volkszaehler/mbmd.svg?branch=master)](https://travis-ci.org/volkszaehler/mbmd) -A daemon for collecting measurement data from smart meters and grid inverters over modbus. +A daemon for collecting measurement data from smart meters, grid inverters and SolarEdge batteries over modbus. `mbmd` provides an http interface to smart meters and grid inverters with modbus interface. Meter readings are made accessible through REST API and MQTT. @@ -27,7 +27,7 @@ Modbus communication is possible over RS485 connections as well as TCP sockets. ## Requirements You'll need: -* A supported Modbus/RTU smart meter OR an supported Modbus/TCP SunSpec-compatible grid inverter. +* A supported Modbus/RTU smart meter OR a supported Modbus/TCP SunSpec-compatible grid inverter. * In case of Modbus/RTU: A USB RS485 adapter. See [USB-ISO-RS485 project](https://github.com/gonium/usb-iso-rs485) for a home-grown adapter. * Optionally an RS485 to Ethernet converter (see [SO discussion](https://stackoverflow.com/questions/59459877/is-rtu-over-tcp-a-spec-conforming-modbus-application)) @@ -310,6 +310,9 @@ SunSpec devices can host multiple subdevices, e.g. to expose a meter attached to ./mbmd run -a 192.168.0.44:502 -d FRONIUS:1.0 -d FRONIUS:1.1 +SolarEdge batteries can be included with other devices: + + ./mbmd run -d SUNS:1,SUNS:1.1,SUNS:2,SE-BAT:1,SE-BAT:1.1 -a 192.168.168.168:1502 --timeout 10s --mqtt-clientid mbmd --mqtt-homie homie --mqtt-topic mbmd --mqtt-broker tcp://192.168.168.169:1883 # Releases diff --git a/cmd/confighandler.go b/cmd/confighandler.go index 284debb9..40c38873 100644 --- a/cmd/confighandler.go +++ b/cmd/confighandler.go @@ -126,7 +126,8 @@ func (conf *DeviceConfigHandler) createDeviceForManager( meterType = strings.ToUpper(meterType) var isSunspec bool - sunspecTypes := []string{"FRONIUS", "KOSTAL", "KACO", "SE", "SMA", "SOLAREDGE", "STECA", "SUNS", "SUNSPEC"} + // Modify this line to add the battery device types: + sunspecTypes := []string{"FRONIUS", "KOSTAL", "KACO", "SE", "SMA", "SOLAREDGE", "STECA", "SUNS", "SUNSPEC", "SE-BAT", "SOLAREDGE-BAT", "SE-BATTERY", "SOLAREDGE-BATTERY"} for _, t := range sunspecTypes { if t == meterType { isSunspec = true diff --git a/cmd/scan.go b/cmd/scan.go index bf3d72fd..31d86003 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -79,7 +79,7 @@ func scan(cmd *cobra.Command, args []string) { // create devices devices := make([]meters.Device, 0) if _, ok := conn.(*meters.TCP); ok { - suns := sunspec.NewDevice("SUNS") + suns := sunspec.NewDevice("SUNS", 0) // Pass 0 as the default subdevice devices = append(devices, suns) } else { for t := range rs485.Producers { diff --git a/meters/measurement_enumer.go b/meters/measurement_enumer.go index f7d03e89..2cd89e51 100644 --- a/meters/measurement_enumer.go +++ b/meters/measurement_enumer.go @@ -7,11 +7,11 @@ import ( "strings" ) -const _MeasurementName = "FrequencyCurrentCurrentL1CurrentL2CurrentL3VoltageVoltageL1VoltageL2VoltageL3PowerPowerL1PowerL2PowerL3ImportPowerImportPowerL1ImportPowerL2ImportPowerL3ExportPowerExportPowerL1ExportPowerL2ExportPowerL3ReactivePowerReactivePowerL1ReactivePowerL2ReactivePowerL3ApparentPowerApparentPowerL1ApparentPowerL2ApparentPowerL3CosphiCosphiL1CosphiL2CosphiL3THDTHDL1THDL2THDL3SumSumT1SumT2SumL1SumL2SumL3ImportImportT1ImportT2ImportL1ImportL2ImportL3ExportExportT1ExportT2ExportL1ExportL2ExportL3ReactiveSumReactiveSumT1ReactiveSumT2ReactiveSumL1ReactiveSumL2ReactiveSumL3ReactiveImportReactiveImportT1ReactiveImportT2ReactiveImportL1ReactiveImportL2ReactiveImportL3ReactiveExportReactiveExportT1ReactiveExportT2ReactiveExportL1ReactiveExportL2ReactiveExportL3DCCurrentDCVoltageDCPowerHeatSinkTempDCCurrentS1DCVoltageS1DCPowerS1DCEnergyS1DCCurrentS2DCVoltageS2DCPowerS2DCEnergyS2DCCurrentS3DCVoltageS3DCPowerS3DCEnergyS3DCCurrentS4DCVoltageS4DCPowerS4DCEnergyS4ChargeStateBatteryVoltagePhaseAngle" +const _MeasurementName = "FrequencyCurrentCurrentL1CurrentL2CurrentL3VoltageVoltageL1VoltageL2VoltageL3PowerPowerL1PowerL2PowerL3ImportPowerImportPowerL1ImportPowerL2ImportPowerL3ExportPowerExportPowerL1ExportPowerL2ExportPowerL3ReactivePowerReactivePowerL1ReactivePowerL2ReactivePowerL3ApparentPowerApparentPowerL1ApparentPowerL2ApparentPowerL3CosphiCosphiL1CosphiL2CosphiL3THDTHDL1THDL2THDL3SumSumT1SumT2SumL1SumL2SumL3ImportImportT1ImportT2ImportL1ImportL2ImportL3ExportExportT1ExportT2ExportL1ExportL2ExportL3ReactiveSumReactiveSumT1ReactiveSumT2ReactiveSumL1ReactiveSumL2ReactiveSumL3ReactiveImportReactiveImportT1ReactiveImportT2ReactiveImportL1ReactiveImportL2ReactiveImportL3ReactiveExportReactiveExportT1ReactiveExportT2ReactiveExportL1ReactiveExportL2ReactiveExportL3DCCurrentDCVoltageDCPowerHeatSinkTempDCCurrentS1DCVoltageS1DCPowerS1DCEnergyS1DCCurrentS2DCVoltageS2DCPowerS2DCEnergyS2DCCurrentS3DCVoltageS3DCPowerS3DCEnergyS3DCCurrentS4DCVoltageS4DCPowerS4DCEnergyS4ChargeStateBatteryVoltagePhaseAngleBatterySOHBatterySOCBatteryTemperatureBatteryCurrentBatteryPowerBatteryStatusBatteryCriticalFaultBatteryWarningFaultBatteryAlertFaultBatteryRatedCapacityBatteryMaxPowerChargeBatteryMaxPowerDischargeBatteryAvailableEnergy" -var _MeasurementIndex = [...]uint16{0, 9, 16, 25, 34, 43, 50, 59, 68, 77, 82, 89, 96, 103, 114, 127, 140, 153, 164, 177, 190, 203, 216, 231, 246, 261, 274, 289, 304, 319, 325, 333, 341, 349, 352, 357, 362, 367, 370, 375, 380, 385, 390, 395, 401, 409, 417, 425, 433, 441, 447, 455, 463, 471, 479, 487, 498, 511, 524, 537, 550, 563, 577, 593, 609, 625, 641, 657, 671, 687, 703, 719, 735, 751, 760, 769, 776, 788, 799, 810, 819, 829, 840, 851, 860, 870, 881, 892, 901, 911, 922, 933, 942, 952, 963, 977, 987} +var _MeasurementIndex = [...]uint16{0, 9, 16, 25, 34, 43, 50, 59, 68, 77, 82, 89, 96, 103, 114, 127, 140, 153, 164, 177, 190, 203, 216, 231, 246, 261, 274, 289, 304, 319, 325, 333, 341, 349, 352, 357, 362, 367, 370, 375, 380, 385, 390, 395, 401, 409, 417, 425, 433, 441, 447, 455, 463, 471, 479, 487, 498, 511, 524, 537, 550, 563, 577, 593, 609, 625, 641, 657, 671, 687, 703, 719, 735, 751, 760, 769, 776, 788, 799, 810, 819, 829, 840, 851, 860, 870, 881, 892, 901, 911, 922, 933, 942, 952, 963, 977, 987, 997, 1007, 1025, 1039, 1051, 1064, 1084, 1103, 1120, 1140, 1161, 1185, 1207} -const _MeasurementLowerName = "frequencycurrentcurrentl1currentl2currentl3voltagevoltagel1voltagel2voltagel3powerpowerl1powerl2powerl3importpowerimportpowerl1importpowerl2importpowerl3exportpowerexportpowerl1exportpowerl2exportpowerl3reactivepowerreactivepowerl1reactivepowerl2reactivepowerl3apparentpowerapparentpowerl1apparentpowerl2apparentpowerl3cosphicosphil1cosphil2cosphil3thdthdl1thdl2thdl3sumsumt1sumt2suml1suml2suml3importimportt1importt2importl1importl2importl3exportexportt1exportt2exportl1exportl2exportl3reactivesumreactivesumt1reactivesumt2reactivesuml1reactivesuml2reactivesuml3reactiveimportreactiveimportt1reactiveimportt2reactiveimportl1reactiveimportl2reactiveimportl3reactiveexportreactiveexportt1reactiveexportt2reactiveexportl1reactiveexportl2reactiveexportl3dccurrentdcvoltagedcpowerheatsinktempdccurrents1dcvoltages1dcpowers1dcenergys1dccurrents2dcvoltages2dcpowers2dcenergys2dccurrents3dcvoltages3dcpowers3dcenergys3dccurrents4dcvoltages4dcpowers4dcenergys4chargestatebatteryvoltagephaseangle" +const _MeasurementLowerName = "frequencycurrentcurrentl1currentl2currentl3voltagevoltagel1voltagel2voltagel3powerpowerl1powerl2powerl3importpowerimportpowerl1importpowerl2importpowerl3exportpowerexportpowerl1exportpowerl2exportpowerl3reactivepowerreactivepowerl1reactivepowerl2reactivepowerl3apparentpowerapparentpowerl1apparentpowerl2apparentpowerl3cosphicosphil1cosphil2cosphil3thdthdl1thdl2thdl3sumsumt1sumt2suml1suml2suml3importimportt1importt2importl1importl2importl3exportexportt1exportt2exportl1exportl2exportl3reactivesumreactivesumt1reactivesumt2reactivesuml1reactivesuml2reactivesuml3reactiveimportreactiveimportt1reactiveimportt2reactiveimportl1reactiveimportl2reactiveimportl3reactiveexportreactiveexportt1reactiveexportt2reactiveexportl1reactiveexportl2reactiveexportl3dccurrentdcvoltagedcpowerheatsinktempdccurrents1dcvoltages1dcpowers1dcenergys1dccurrents2dcvoltages2dcpowers2dcenergys2dccurrents3dcvoltages3dcpowers3dcenergys3dccurrents4dcvoltages4dcpowers4dcenergys4chargestatebatteryvoltagephaseanglebatterysohbatterysocbatterytemperaturebatterycurrentbatterypowerbatterystatusbatterycriticalfaultbatterywarningfaultbatteryalertfaultbatteryratedcapacitybatterymaxpowerchargebatterymaxpowerdischargebatteryavailableenergy" func (i Measurement) String() string { i -= 1 @@ -121,203 +121,242 @@ func _MeasurementNoOp() { _ = x[ChargeState-(94)] _ = x[BatteryVoltage-(95)] _ = x[PhaseAngle-(96)] + _ = x[BatterySOH-(97)] + _ = x[BatterySOC-(98)] + _ = x[BatteryTemperature-(99)] + _ = x[BatteryCurrent-(100)] + _ = x[BatteryPower-(101)] + _ = x[BatteryStatus-(102)] + _ = x[BatteryCriticalFault-(103)] + _ = x[BatteryWarningFault-(104)] + _ = x[BatteryAlertFault-(105)] + _ = x[BatteryRatedCapacity-(106)] + _ = x[BatteryMaxPowerCharge-(107)] + _ = x[BatteryMaxPowerDischarge-(108)] + _ = x[BatteryAvailableEnergy-(109)] } -var _MeasurementValues = []Measurement{Frequency, Current, CurrentL1, CurrentL2, CurrentL3, Voltage, VoltageL1, VoltageL2, VoltageL3, Power, PowerL1, PowerL2, PowerL3, ImportPower, ImportPowerL1, ImportPowerL2, ImportPowerL3, ExportPower, ExportPowerL1, ExportPowerL2, ExportPowerL3, ReactivePower, ReactivePowerL1, ReactivePowerL2, ReactivePowerL3, ApparentPower, ApparentPowerL1, ApparentPowerL2, ApparentPowerL3, Cosphi, CosphiL1, CosphiL2, CosphiL3, THD, THDL1, THDL2, THDL3, Sum, SumT1, SumT2, SumL1, SumL2, SumL3, Import, ImportT1, ImportT2, ImportL1, ImportL2, ImportL3, Export, ExportT1, ExportT2, ExportL1, ExportL2, ExportL3, ReactiveSum, ReactiveSumT1, ReactiveSumT2, ReactiveSumL1, ReactiveSumL2, ReactiveSumL3, ReactiveImport, ReactiveImportT1, ReactiveImportT2, ReactiveImportL1, ReactiveImportL2, ReactiveImportL3, ReactiveExport, ReactiveExportT1, ReactiveExportT2, ReactiveExportL1, ReactiveExportL2, ReactiveExportL3, DCCurrent, DCVoltage, DCPower, HeatSinkTemp, DCCurrentS1, DCVoltageS1, DCPowerS1, DCEnergyS1, DCCurrentS2, DCVoltageS2, DCPowerS2, DCEnergyS2, DCCurrentS3, DCVoltageS3, DCPowerS3, DCEnergyS3, DCCurrentS4, DCVoltageS4, DCPowerS4, DCEnergyS4, ChargeState, BatteryVoltage, PhaseAngle} +var _MeasurementValues = []Measurement{Frequency, Current, CurrentL1, CurrentL2, CurrentL3, Voltage, VoltageL1, VoltageL2, VoltageL3, Power, PowerL1, PowerL2, PowerL3, ImportPower, ImportPowerL1, ImportPowerL2, ImportPowerL3, ExportPower, ExportPowerL1, ExportPowerL2, ExportPowerL3, ReactivePower, ReactivePowerL1, ReactivePowerL2, ReactivePowerL3, ApparentPower, ApparentPowerL1, ApparentPowerL2, ApparentPowerL3, Cosphi, CosphiL1, CosphiL2, CosphiL3, THD, THDL1, THDL2, THDL3, Sum, SumT1, SumT2, SumL1, SumL2, SumL3, Import, ImportT1, ImportT2, ImportL1, ImportL2, ImportL3, Export, ExportT1, ExportT2, ExportL1, ExportL2, ExportL3, ReactiveSum, ReactiveSumT1, ReactiveSumT2, ReactiveSumL1, ReactiveSumL2, ReactiveSumL3, ReactiveImport, ReactiveImportT1, ReactiveImportT2, ReactiveImportL1, ReactiveImportL2, ReactiveImportL3, ReactiveExport, ReactiveExportT1, ReactiveExportT2, ReactiveExportL1, ReactiveExportL2, ReactiveExportL3, DCCurrent, DCVoltage, DCPower, HeatSinkTemp, DCCurrentS1, DCVoltageS1, DCPowerS1, DCEnergyS1, DCCurrentS2, DCVoltageS2, DCPowerS2, DCEnergyS2, DCCurrentS3, DCVoltageS3, DCPowerS3, DCEnergyS3, DCCurrentS4, DCVoltageS4, DCPowerS4, DCEnergyS4, ChargeState, BatteryVoltage, PhaseAngle, BatterySOH, BatterySOC, BatteryTemperature, BatteryCurrent, BatteryPower, BatteryStatus, BatteryCriticalFault, BatteryWarningFault, BatteryAlertFault, BatteryRatedCapacity, BatteryMaxPowerCharge, BatteryMaxPowerDischarge, BatteryAvailableEnergy} var _MeasurementNameToValueMap = map[string]Measurement{ - _MeasurementName[0:9]: Frequency, - _MeasurementLowerName[0:9]: Frequency, - _MeasurementName[9:16]: Current, - _MeasurementLowerName[9:16]: Current, - _MeasurementName[16:25]: CurrentL1, - _MeasurementLowerName[16:25]: CurrentL1, - _MeasurementName[25:34]: CurrentL2, - _MeasurementLowerName[25:34]: CurrentL2, - _MeasurementName[34:43]: CurrentL3, - _MeasurementLowerName[34:43]: CurrentL3, - _MeasurementName[43:50]: Voltage, - _MeasurementLowerName[43:50]: Voltage, - _MeasurementName[50:59]: VoltageL1, - _MeasurementLowerName[50:59]: VoltageL1, - _MeasurementName[59:68]: VoltageL2, - _MeasurementLowerName[59:68]: VoltageL2, - _MeasurementName[68:77]: VoltageL3, - _MeasurementLowerName[68:77]: VoltageL3, - _MeasurementName[77:82]: Power, - _MeasurementLowerName[77:82]: Power, - _MeasurementName[82:89]: PowerL1, - _MeasurementLowerName[82:89]: PowerL1, - _MeasurementName[89:96]: PowerL2, - _MeasurementLowerName[89:96]: PowerL2, - _MeasurementName[96:103]: PowerL3, - _MeasurementLowerName[96:103]: PowerL3, - _MeasurementName[103:114]: ImportPower, - _MeasurementLowerName[103:114]: ImportPower, - _MeasurementName[114:127]: ImportPowerL1, - _MeasurementLowerName[114:127]: ImportPowerL1, - _MeasurementName[127:140]: ImportPowerL2, - _MeasurementLowerName[127:140]: ImportPowerL2, - _MeasurementName[140:153]: ImportPowerL3, - _MeasurementLowerName[140:153]: ImportPowerL3, - _MeasurementName[153:164]: ExportPower, - _MeasurementLowerName[153:164]: ExportPower, - _MeasurementName[164:177]: ExportPowerL1, - _MeasurementLowerName[164:177]: ExportPowerL1, - _MeasurementName[177:190]: ExportPowerL2, - _MeasurementLowerName[177:190]: ExportPowerL2, - _MeasurementName[190:203]: ExportPowerL3, - _MeasurementLowerName[190:203]: ExportPowerL3, - _MeasurementName[203:216]: ReactivePower, - _MeasurementLowerName[203:216]: ReactivePower, - _MeasurementName[216:231]: ReactivePowerL1, - _MeasurementLowerName[216:231]: ReactivePowerL1, - _MeasurementName[231:246]: ReactivePowerL2, - _MeasurementLowerName[231:246]: ReactivePowerL2, - _MeasurementName[246:261]: ReactivePowerL3, - _MeasurementLowerName[246:261]: ReactivePowerL3, - _MeasurementName[261:274]: ApparentPower, - _MeasurementLowerName[261:274]: ApparentPower, - _MeasurementName[274:289]: ApparentPowerL1, - _MeasurementLowerName[274:289]: ApparentPowerL1, - _MeasurementName[289:304]: ApparentPowerL2, - _MeasurementLowerName[289:304]: ApparentPowerL2, - _MeasurementName[304:319]: ApparentPowerL3, - _MeasurementLowerName[304:319]: ApparentPowerL3, - _MeasurementName[319:325]: Cosphi, - _MeasurementLowerName[319:325]: Cosphi, - _MeasurementName[325:333]: CosphiL1, - _MeasurementLowerName[325:333]: CosphiL1, - _MeasurementName[333:341]: CosphiL2, - _MeasurementLowerName[333:341]: CosphiL2, - _MeasurementName[341:349]: CosphiL3, - _MeasurementLowerName[341:349]: CosphiL3, - _MeasurementName[349:352]: THD, - _MeasurementLowerName[349:352]: THD, - _MeasurementName[352:357]: THDL1, - _MeasurementLowerName[352:357]: THDL1, - _MeasurementName[357:362]: THDL2, - _MeasurementLowerName[357:362]: THDL2, - _MeasurementName[362:367]: THDL3, - _MeasurementLowerName[362:367]: THDL3, - _MeasurementName[367:370]: Sum, - _MeasurementLowerName[367:370]: Sum, - _MeasurementName[370:375]: SumT1, - _MeasurementLowerName[370:375]: SumT1, - _MeasurementName[375:380]: SumT2, - _MeasurementLowerName[375:380]: SumT2, - _MeasurementName[380:385]: SumL1, - _MeasurementLowerName[380:385]: SumL1, - _MeasurementName[385:390]: SumL2, - _MeasurementLowerName[385:390]: SumL2, - _MeasurementName[390:395]: SumL3, - _MeasurementLowerName[390:395]: SumL3, - _MeasurementName[395:401]: Import, - _MeasurementLowerName[395:401]: Import, - _MeasurementName[401:409]: ImportT1, - _MeasurementLowerName[401:409]: ImportT1, - _MeasurementName[409:417]: ImportT2, - _MeasurementLowerName[409:417]: ImportT2, - _MeasurementName[417:425]: ImportL1, - _MeasurementLowerName[417:425]: ImportL1, - _MeasurementName[425:433]: ImportL2, - _MeasurementLowerName[425:433]: ImportL2, - _MeasurementName[433:441]: ImportL3, - _MeasurementLowerName[433:441]: ImportL3, - _MeasurementName[441:447]: Export, - _MeasurementLowerName[441:447]: Export, - _MeasurementName[447:455]: ExportT1, - _MeasurementLowerName[447:455]: ExportT1, - _MeasurementName[455:463]: ExportT2, - _MeasurementLowerName[455:463]: ExportT2, - _MeasurementName[463:471]: ExportL1, - _MeasurementLowerName[463:471]: ExportL1, - _MeasurementName[471:479]: ExportL2, - _MeasurementLowerName[471:479]: ExportL2, - _MeasurementName[479:487]: ExportL3, - _MeasurementLowerName[479:487]: ExportL3, - _MeasurementName[487:498]: ReactiveSum, - _MeasurementLowerName[487:498]: ReactiveSum, - _MeasurementName[498:511]: ReactiveSumT1, - _MeasurementLowerName[498:511]: ReactiveSumT1, - _MeasurementName[511:524]: ReactiveSumT2, - _MeasurementLowerName[511:524]: ReactiveSumT2, - _MeasurementName[524:537]: ReactiveSumL1, - _MeasurementLowerName[524:537]: ReactiveSumL1, - _MeasurementName[537:550]: ReactiveSumL2, - _MeasurementLowerName[537:550]: ReactiveSumL2, - _MeasurementName[550:563]: ReactiveSumL3, - _MeasurementLowerName[550:563]: ReactiveSumL3, - _MeasurementName[563:577]: ReactiveImport, - _MeasurementLowerName[563:577]: ReactiveImport, - _MeasurementName[577:593]: ReactiveImportT1, - _MeasurementLowerName[577:593]: ReactiveImportT1, - _MeasurementName[593:609]: ReactiveImportT2, - _MeasurementLowerName[593:609]: ReactiveImportT2, - _MeasurementName[609:625]: ReactiveImportL1, - _MeasurementLowerName[609:625]: ReactiveImportL1, - _MeasurementName[625:641]: ReactiveImportL2, - _MeasurementLowerName[625:641]: ReactiveImportL2, - _MeasurementName[641:657]: ReactiveImportL3, - _MeasurementLowerName[641:657]: ReactiveImportL3, - _MeasurementName[657:671]: ReactiveExport, - _MeasurementLowerName[657:671]: ReactiveExport, - _MeasurementName[671:687]: ReactiveExportT1, - _MeasurementLowerName[671:687]: ReactiveExportT1, - _MeasurementName[687:703]: ReactiveExportT2, - _MeasurementLowerName[687:703]: ReactiveExportT2, - _MeasurementName[703:719]: ReactiveExportL1, - _MeasurementLowerName[703:719]: ReactiveExportL1, - _MeasurementName[719:735]: ReactiveExportL2, - _MeasurementLowerName[719:735]: ReactiveExportL2, - _MeasurementName[735:751]: ReactiveExportL3, - _MeasurementLowerName[735:751]: ReactiveExportL3, - _MeasurementName[751:760]: DCCurrent, - _MeasurementLowerName[751:760]: DCCurrent, - _MeasurementName[760:769]: DCVoltage, - _MeasurementLowerName[760:769]: DCVoltage, - _MeasurementName[769:776]: DCPower, - _MeasurementLowerName[769:776]: DCPower, - _MeasurementName[776:788]: HeatSinkTemp, - _MeasurementLowerName[776:788]: HeatSinkTemp, - _MeasurementName[788:799]: DCCurrentS1, - _MeasurementLowerName[788:799]: DCCurrentS1, - _MeasurementName[799:810]: DCVoltageS1, - _MeasurementLowerName[799:810]: DCVoltageS1, - _MeasurementName[810:819]: DCPowerS1, - _MeasurementLowerName[810:819]: DCPowerS1, - _MeasurementName[819:829]: DCEnergyS1, - _MeasurementLowerName[819:829]: DCEnergyS1, - _MeasurementName[829:840]: DCCurrentS2, - _MeasurementLowerName[829:840]: DCCurrentS2, - _MeasurementName[840:851]: DCVoltageS2, - _MeasurementLowerName[840:851]: DCVoltageS2, - _MeasurementName[851:860]: DCPowerS2, - _MeasurementLowerName[851:860]: DCPowerS2, - _MeasurementName[860:870]: DCEnergyS2, - _MeasurementLowerName[860:870]: DCEnergyS2, - _MeasurementName[870:881]: DCCurrentS3, - _MeasurementLowerName[870:881]: DCCurrentS3, - _MeasurementName[881:892]: DCVoltageS3, - _MeasurementLowerName[881:892]: DCVoltageS3, - _MeasurementName[892:901]: DCPowerS3, - _MeasurementLowerName[892:901]: DCPowerS3, - _MeasurementName[901:911]: DCEnergyS3, - _MeasurementLowerName[901:911]: DCEnergyS3, - _MeasurementName[911:922]: DCCurrentS4, - _MeasurementLowerName[911:922]: DCCurrentS4, - _MeasurementName[922:933]: DCVoltageS4, - _MeasurementLowerName[922:933]: DCVoltageS4, - _MeasurementName[933:942]: DCPowerS4, - _MeasurementLowerName[933:942]: DCPowerS4, - _MeasurementName[942:952]: DCEnergyS4, - _MeasurementLowerName[942:952]: DCEnergyS4, - _MeasurementName[952:963]: ChargeState, - _MeasurementLowerName[952:963]: ChargeState, - _MeasurementName[963:977]: BatteryVoltage, - _MeasurementLowerName[963:977]: BatteryVoltage, - _MeasurementName[977:987]: PhaseAngle, - _MeasurementLowerName[977:987]: PhaseAngle, + _MeasurementName[0:9]: Frequency, + _MeasurementLowerName[0:9]: Frequency, + _MeasurementName[9:16]: Current, + _MeasurementLowerName[9:16]: Current, + _MeasurementName[16:25]: CurrentL1, + _MeasurementLowerName[16:25]: CurrentL1, + _MeasurementName[25:34]: CurrentL2, + _MeasurementLowerName[25:34]: CurrentL2, + _MeasurementName[34:43]: CurrentL3, + _MeasurementLowerName[34:43]: CurrentL3, + _MeasurementName[43:50]: Voltage, + _MeasurementLowerName[43:50]: Voltage, + _MeasurementName[50:59]: VoltageL1, + _MeasurementLowerName[50:59]: VoltageL1, + _MeasurementName[59:68]: VoltageL2, + _MeasurementLowerName[59:68]: VoltageL2, + _MeasurementName[68:77]: VoltageL3, + _MeasurementLowerName[68:77]: VoltageL3, + _MeasurementName[77:82]: Power, + _MeasurementLowerName[77:82]: Power, + _MeasurementName[82:89]: PowerL1, + _MeasurementLowerName[82:89]: PowerL1, + _MeasurementName[89:96]: PowerL2, + _MeasurementLowerName[89:96]: PowerL2, + _MeasurementName[96:103]: PowerL3, + _MeasurementLowerName[96:103]: PowerL3, + _MeasurementName[103:114]: ImportPower, + _MeasurementLowerName[103:114]: ImportPower, + _MeasurementName[114:127]: ImportPowerL1, + _MeasurementLowerName[114:127]: ImportPowerL1, + _MeasurementName[127:140]: ImportPowerL2, + _MeasurementLowerName[127:140]: ImportPowerL2, + _MeasurementName[140:153]: ImportPowerL3, + _MeasurementLowerName[140:153]: ImportPowerL3, + _MeasurementName[153:164]: ExportPower, + _MeasurementLowerName[153:164]: ExportPower, + _MeasurementName[164:177]: ExportPowerL1, + _MeasurementLowerName[164:177]: ExportPowerL1, + _MeasurementName[177:190]: ExportPowerL2, + _MeasurementLowerName[177:190]: ExportPowerL2, + _MeasurementName[190:203]: ExportPowerL3, + _MeasurementLowerName[190:203]: ExportPowerL3, + _MeasurementName[203:216]: ReactivePower, + _MeasurementLowerName[203:216]: ReactivePower, + _MeasurementName[216:231]: ReactivePowerL1, + _MeasurementLowerName[216:231]: ReactivePowerL1, + _MeasurementName[231:246]: ReactivePowerL2, + _MeasurementLowerName[231:246]: ReactivePowerL2, + _MeasurementName[246:261]: ReactivePowerL3, + _MeasurementLowerName[246:261]: ReactivePowerL3, + _MeasurementName[261:274]: ApparentPower, + _MeasurementLowerName[261:274]: ApparentPower, + _MeasurementName[274:289]: ApparentPowerL1, + _MeasurementLowerName[274:289]: ApparentPowerL1, + _MeasurementName[289:304]: ApparentPowerL2, + _MeasurementLowerName[289:304]: ApparentPowerL2, + _MeasurementName[304:319]: ApparentPowerL3, + _MeasurementLowerName[304:319]: ApparentPowerL3, + _MeasurementName[319:325]: Cosphi, + _MeasurementLowerName[319:325]: Cosphi, + _MeasurementName[325:333]: CosphiL1, + _MeasurementLowerName[325:333]: CosphiL1, + _MeasurementName[333:341]: CosphiL2, + _MeasurementLowerName[333:341]: CosphiL2, + _MeasurementName[341:349]: CosphiL3, + _MeasurementLowerName[341:349]: CosphiL3, + _MeasurementName[349:352]: THD, + _MeasurementLowerName[349:352]: THD, + _MeasurementName[352:357]: THDL1, + _MeasurementLowerName[352:357]: THDL1, + _MeasurementName[357:362]: THDL2, + _MeasurementLowerName[357:362]: THDL2, + _MeasurementName[362:367]: THDL3, + _MeasurementLowerName[362:367]: THDL3, + _MeasurementName[367:370]: Sum, + _MeasurementLowerName[367:370]: Sum, + _MeasurementName[370:375]: SumT1, + _MeasurementLowerName[370:375]: SumT1, + _MeasurementName[375:380]: SumT2, + _MeasurementLowerName[375:380]: SumT2, + _MeasurementName[380:385]: SumL1, + _MeasurementLowerName[380:385]: SumL1, + _MeasurementName[385:390]: SumL2, + _MeasurementLowerName[385:390]: SumL2, + _MeasurementName[390:395]: SumL3, + _MeasurementLowerName[390:395]: SumL3, + _MeasurementName[395:401]: Import, + _MeasurementLowerName[395:401]: Import, + _MeasurementName[401:409]: ImportT1, + _MeasurementLowerName[401:409]: ImportT1, + _MeasurementName[409:417]: ImportT2, + _MeasurementLowerName[409:417]: ImportT2, + _MeasurementName[417:425]: ImportL1, + _MeasurementLowerName[417:425]: ImportL1, + _MeasurementName[425:433]: ImportL2, + _MeasurementLowerName[425:433]: ImportL2, + _MeasurementName[433:441]: ImportL3, + _MeasurementLowerName[433:441]: ImportL3, + _MeasurementName[441:447]: Export, + _MeasurementLowerName[441:447]: Export, + _MeasurementName[447:455]: ExportT1, + _MeasurementLowerName[447:455]: ExportT1, + _MeasurementName[455:463]: ExportT2, + _MeasurementLowerName[455:463]: ExportT2, + _MeasurementName[463:471]: ExportL1, + _MeasurementLowerName[463:471]: ExportL1, + _MeasurementName[471:479]: ExportL2, + _MeasurementLowerName[471:479]: ExportL2, + _MeasurementName[479:487]: ExportL3, + _MeasurementLowerName[479:487]: ExportL3, + _MeasurementName[487:498]: ReactiveSum, + _MeasurementLowerName[487:498]: ReactiveSum, + _MeasurementName[498:511]: ReactiveSumT1, + _MeasurementLowerName[498:511]: ReactiveSumT1, + _MeasurementName[511:524]: ReactiveSumT2, + _MeasurementLowerName[511:524]: ReactiveSumT2, + _MeasurementName[524:537]: ReactiveSumL1, + _MeasurementLowerName[524:537]: ReactiveSumL1, + _MeasurementName[537:550]: ReactiveSumL2, + _MeasurementLowerName[537:550]: ReactiveSumL2, + _MeasurementName[550:563]: ReactiveSumL3, + _MeasurementLowerName[550:563]: ReactiveSumL3, + _MeasurementName[563:577]: ReactiveImport, + _MeasurementLowerName[563:577]: ReactiveImport, + _MeasurementName[577:593]: ReactiveImportT1, + _MeasurementLowerName[577:593]: ReactiveImportT1, + _MeasurementName[593:609]: ReactiveImportT2, + _MeasurementLowerName[593:609]: ReactiveImportT2, + _MeasurementName[609:625]: ReactiveImportL1, + _MeasurementLowerName[609:625]: ReactiveImportL1, + _MeasurementName[625:641]: ReactiveImportL2, + _MeasurementLowerName[625:641]: ReactiveImportL2, + _MeasurementName[641:657]: ReactiveImportL3, + _MeasurementLowerName[641:657]: ReactiveImportL3, + _MeasurementName[657:671]: ReactiveExport, + _MeasurementLowerName[657:671]: ReactiveExport, + _MeasurementName[671:687]: ReactiveExportT1, + _MeasurementLowerName[671:687]: ReactiveExportT1, + _MeasurementName[687:703]: ReactiveExportT2, + _MeasurementLowerName[687:703]: ReactiveExportT2, + _MeasurementName[703:719]: ReactiveExportL1, + _MeasurementLowerName[703:719]: ReactiveExportL1, + _MeasurementName[719:735]: ReactiveExportL2, + _MeasurementLowerName[719:735]: ReactiveExportL2, + _MeasurementName[735:751]: ReactiveExportL3, + _MeasurementLowerName[735:751]: ReactiveExportL3, + _MeasurementName[751:760]: DCCurrent, + _MeasurementLowerName[751:760]: DCCurrent, + _MeasurementName[760:769]: DCVoltage, + _MeasurementLowerName[760:769]: DCVoltage, + _MeasurementName[769:776]: DCPower, + _MeasurementLowerName[769:776]: DCPower, + _MeasurementName[776:788]: HeatSinkTemp, + _MeasurementLowerName[776:788]: HeatSinkTemp, + _MeasurementName[788:799]: DCCurrentS1, + _MeasurementLowerName[788:799]: DCCurrentS1, + _MeasurementName[799:810]: DCVoltageS1, + _MeasurementLowerName[799:810]: DCVoltageS1, + _MeasurementName[810:819]: DCPowerS1, + _MeasurementLowerName[810:819]: DCPowerS1, + _MeasurementName[819:829]: DCEnergyS1, + _MeasurementLowerName[819:829]: DCEnergyS1, + _MeasurementName[829:840]: DCCurrentS2, + _MeasurementLowerName[829:840]: DCCurrentS2, + _MeasurementName[840:851]: DCVoltageS2, + _MeasurementLowerName[840:851]: DCVoltageS2, + _MeasurementName[851:860]: DCPowerS2, + _MeasurementLowerName[851:860]: DCPowerS2, + _MeasurementName[860:870]: DCEnergyS2, + _MeasurementLowerName[860:870]: DCEnergyS2, + _MeasurementName[870:881]: DCCurrentS3, + _MeasurementLowerName[870:881]: DCCurrentS3, + _MeasurementName[881:892]: DCVoltageS3, + _MeasurementLowerName[881:892]: DCVoltageS3, + _MeasurementName[892:901]: DCPowerS3, + _MeasurementLowerName[892:901]: DCPowerS3, + _MeasurementName[901:911]: DCEnergyS3, + _MeasurementLowerName[901:911]: DCEnergyS3, + _MeasurementName[911:922]: DCCurrentS4, + _MeasurementLowerName[911:922]: DCCurrentS4, + _MeasurementName[922:933]: DCVoltageS4, + _MeasurementLowerName[922:933]: DCVoltageS4, + _MeasurementName[933:942]: DCPowerS4, + _MeasurementLowerName[933:942]: DCPowerS4, + _MeasurementName[942:952]: DCEnergyS4, + _MeasurementLowerName[942:952]: DCEnergyS4, + _MeasurementName[952:963]: ChargeState, + _MeasurementLowerName[952:963]: ChargeState, + _MeasurementName[963:977]: BatteryVoltage, + _MeasurementLowerName[963:977]: BatteryVoltage, + _MeasurementName[977:987]: PhaseAngle, + _MeasurementLowerName[977:987]: PhaseAngle, + _MeasurementName[987:997]: BatterySOH, + _MeasurementLowerName[987:997]: BatterySOH, + _MeasurementName[997:1007]: BatterySOC, + _MeasurementLowerName[997:1007]: BatterySOC, + _MeasurementName[1007:1025]: BatteryTemperature, + _MeasurementLowerName[1007:1025]: BatteryTemperature, + _MeasurementName[1025:1039]: BatteryCurrent, + _MeasurementLowerName[1025:1039]: BatteryCurrent, + _MeasurementName[1039:1051]: BatteryPower, + _MeasurementLowerName[1039:1051]: BatteryPower, + _MeasurementName[1051:1064]: BatteryStatus, + _MeasurementLowerName[1051:1064]: BatteryStatus, + _MeasurementName[1064:1084]: BatteryCriticalFault, + _MeasurementLowerName[1064:1084]: BatteryCriticalFault, + _MeasurementName[1084:1103]: BatteryWarningFault, + _MeasurementLowerName[1084:1103]: BatteryWarningFault, + _MeasurementName[1103:1120]: BatteryAlertFault, + _MeasurementLowerName[1103:1120]: BatteryAlertFault, + _MeasurementName[1120:1140]: BatteryRatedCapacity, + _MeasurementLowerName[1120:1140]: BatteryRatedCapacity, + _MeasurementName[1140:1161]: BatteryMaxPowerCharge, + _MeasurementLowerName[1140:1161]: BatteryMaxPowerCharge, + _MeasurementName[1161:1185]: BatteryMaxPowerDischarge, + _MeasurementLowerName[1161:1185]: BatteryMaxPowerDischarge, + _MeasurementName[1185:1207]: BatteryAvailableEnergy, + _MeasurementLowerName[1185:1207]: BatteryAvailableEnergy, } var _MeasurementNames = []string{ @@ -417,6 +456,19 @@ var _MeasurementNames = []string{ _MeasurementName[952:963], _MeasurementName[963:977], _MeasurementName[977:987], + _MeasurementName[987:997], + _MeasurementName[997:1007], + _MeasurementName[1007:1025], + _MeasurementName[1025:1039], + _MeasurementName[1039:1051], + _MeasurementName[1051:1064], + _MeasurementName[1064:1084], + _MeasurementName[1084:1103], + _MeasurementName[1103:1120], + _MeasurementName[1120:1140], + _MeasurementName[1140:1161], + _MeasurementName[1161:1185], + _MeasurementName[1185:1207], } // MeasurementString retrieves an enum value from the enum constants string name. diff --git a/meters/measurements.go b/meters/measurements.go index b863c266..9033b91a 100644 --- a/meters/measurements.go +++ b/meters/measurements.go @@ -142,8 +142,22 @@ const ( // Battery ChargeState BatteryVoltage - PhaseAngle + + // Battery specific measurements + BatterySOH // State of Health + BatterySOC // State of Charge + BatteryTemperature // Battery temperature + BatteryCurrent // Battery current + BatteryPower // Battery power + BatteryStatus // Battery status code + BatteryCriticalFault // Battery critical fault + BatteryWarningFault // Battery warning fault + BatteryAlertFault // Battery alert fault + BatteryRatedCapacity // Battery rated capacity + BatteryMaxPowerCharge // Battery max charge power + BatteryMaxPowerDischarge // Battery max discharge power + BatteryAvailableEnergy // Battery available energy ) var iec = map[Measurement][]string{ @@ -243,6 +257,21 @@ var iec = map[Measurement][]string{ ChargeState: {"Charge State", "%"}, BatteryVoltage: {"Battery Voltage", "V"}, PhaseAngle: {"Phase Angle", "°"}, + + // Battery specific measurements + BatterySOH: {"Battery State of Health", "%"}, + BatterySOC: {"Battery State of Charge", "%"}, + BatteryTemperature: {"Battery Temperature", "°C"}, + BatteryCurrent: {"Battery Current", "A"}, + BatteryPower: {"Battery Power", "W"}, + BatteryStatus: {"Battery Status", ""}, + BatteryCriticalFault: {"Battery Critical Fault", ""}, + BatteryWarningFault: {"Battery Warning Fault", ""}, + BatteryAlertFault: {"Battery Alert Fault", ""}, + BatteryRatedCapacity: {"Battery Rated Capacity", "kWh"}, + BatteryMaxPowerCharge: {"Battery Max Power Charge", "W"}, + BatteryMaxPowerDischarge: {"Battery Max Power Discharge", "W"}, + BatteryAvailableEnergy: {"Battery Available Energy", "kWh"}, } // MarshalText implements encoding.TextMarshaler diff --git a/meters/sunspec/solaredge_battery.go b/meters/sunspec/solaredge_battery.go new file mode 100644 index 00000000..4d7d3ceb --- /dev/null +++ b/meters/sunspec/solaredge_battery.go @@ -0,0 +1,266 @@ +package sunspec + +import ( + "encoding/binary" + "fmt" + "math" + "strings" + "time" + + "github.com/grid-x/modbus" + "github.com/volkszaehler/mbmd/meters" +) + +const ( + // Battery register addresses (in decimal, exactly as in the Perl script) + batteryRegisterBase1 = 57600 // 0xE100 - Battery 1 + batteryRegisterBase2 = 57856 // 0xE200 - Battery 2 +) + +// SolarEdgeBattery implements the meters.Device interface for SolarEdge battery +type SolarEdgeBattery struct { + SunSpec +} + +// NewSolarEdgeBatteryDevice creates a SolarEdge battery device +func NewSolarEdgeBatteryDevice(subdevice int) *SolarEdgeBattery { + dev := &SolarEdgeBattery{ + SunSpec: SunSpec{ + subdevice: subdevice, + descriptor: meters.DeviceDescriptor{ + Type: "SE-BAT", + Manufacturer: "SolarEdge", + Model: "Home Battery", + SubDevice: subdevice, + }, + }, + } + + return dev +} + +// readStringFromRegisters reads a string from Modbus registers +func readStringFromRegisters(data []byte) string { + result := "" + + // Process each byte individually + for i := 0; i < len(data); i++ { + // Only add non-null bytes to the string + if data[i] != 0 { + result += string(data[i]) + } + } + + return strings.TrimSpace(result) +} + +// Initialize implements the meters.Device interface +func (d *SolarEdgeBattery) Initialize(client modbus.Client) error { + baseAddr := d.getBaseAddress() + + // Read manufacturer name (16 registers) + manufacturerData, err := client.ReadHoldingRegisters(baseAddr, 16) + if err == nil && len(manufacturerData) >= 32 { + manufacturer := readStringFromRegisters(manufacturerData) + if len(manufacturer) > 0 { + d.descriptor.Manufacturer = manufacturer + } + } + + // Read model name (16 registers) + modelData, err := client.ReadHoldingRegisters(baseAddr + 16, 16) + if err == nil && len(modelData) >= 32 { + model := readStringFromRegisters(modelData) + if len(model) > 0 { + d.descriptor.Model = model + } + } + + // Read firmware version (16 registers) + versionData, err := client.ReadHoldingRegisters(baseAddr + 32, 16) + if err == nil && len(versionData) >= 32 { + version := readStringFromRegisters(versionData) + if len(version) > 0 { + d.descriptor.Version = version + } + } + + // Read serial number (16 registers) + serialData, err := client.ReadHoldingRegisters(baseAddr + 48, 16) + if err == nil && len(serialData) >= 32 { + serial := readStringFromRegisters(serialData) + if len(serial) > 0 { + d.descriptor.Serial = serial + } + } + + return nil +} + +// getBaseAddress returns the correct base register address for the battery +func (d *SolarEdgeBattery) getBaseAddress() uint16 { + if d.subdevice == 0 { + return batteryRegisterBase1 + } + return batteryRegisterBase2 +} + +// readSEFloat reads a SolarEdge float value from the given data at the specified offset +func readSEFloat(data []byte, offset int) (float64, error) { + if offset+3 >= len(data) { + return 0, fmt.Errorf("not enough data") + } + + // Extract as SEFLOAT (little endian) + rawBytes := []byte{data[offset+1], data[offset], data[offset+3], data[offset+2]} + bits := binary.LittleEndian.Uint32(rawBytes) + value := float64(math.Float32frombits(bits)) + + // Skip NaN and Infinity values + if math.IsNaN(value) || math.IsInf(value, 0) { + return 0, fmt.Errorf("invalid value") + } + + return value, nil +} + +// Probe implements the meters.Device interface +func (d *SolarEdgeBattery) Probe(client modbus.Client) (meters.MeasurementResult, error) { + // Try to read the State of Energy register at offset 0x84 (132) + baseAddr := d.getBaseAddress() + + // Read SOE register directly + b, err := client.ReadHoldingRegisters(baseAddr + 0x84, 2) + if err != nil { + return meters.MeasurementResult{}, err + } + + if len(b) < 4 { + return meters.MeasurementResult{}, fmt.Errorf("insufficient data received") + } + + // Extract the SOE value + soc, err := readSEFloat(b, 0) + if err != nil { + return meters.MeasurementResult{}, err + } + + return meters.MeasurementResult{ + Measurement: meters.BatterySOC, + Value: soc, + Timestamp: time.Now(), + }, nil +} + +// Query implements the meters.Device interface +func (d *SolarEdgeBattery) Query(client modbus.Client) ([]meters.MeasurementResult, error) { + res := make([]meters.MeasurementResult, 0, 9) + timestamp := time.Now() + baseAddr := d.getBaseAddress() + + // Read registers in chunks to minimize Modbus traffic + + // First chunk: Read the rated energy and power capabilities + ratedEnergyData, err := client.ReadHoldingRegisters(baseAddr + 0x42, 8) + if err == nil && len(ratedEnergyData) >= 16 { + // Rated Energy (0x42) + if value, err := readSEFloat(ratedEnergyData, 0); err == nil { + res = append(res, meters.MeasurementResult{ + Measurement: meters.BatteryRatedCapacity, + Value: value * 0.001, // Convert Wh to kWh + Timestamp: timestamp, + }) + } + + // Max Charge Power (0x44) + if value, err := readSEFloat(ratedEnergyData, 4); err == nil { + res = append(res, meters.MeasurementResult{ + Measurement: meters.BatteryMaxPowerCharge, + Value: value, + Timestamp: timestamp, + }) + } + + // Max Discharge Power (0x46) + if value, err := readSEFloat(ratedEnergyData, 8); err == nil { + res = append(res, meters.MeasurementResult{ + Measurement: meters.BatteryMaxPowerDischarge, + Value: value, + Timestamp: timestamp, + }) + } + } + + // Second chunk: Read the battery metrics (temperature through voltage, current, power) + batteryData, err := client.ReadHoldingRegisters(baseAddr + 0x6C, 10) + if err == nil && len(batteryData) >= 20 { + // Temperature (0x6C) + if value, err := readSEFloat(batteryData, 0); err == nil { + res = append(res, meters.MeasurementResult{ + Measurement: meters.BatteryTemperature, + Value: value, + Timestamp: timestamp, + }) + } + + // Voltage (0x70) + if value, err := readSEFloat(batteryData, 8); err == nil { + res = append(res, meters.MeasurementResult{ + Measurement: meters.BatteryVoltage, + Value: value, + Timestamp: timestamp, + }) + } + + // Current (0x72) + if value, err := readSEFloat(batteryData, 12); err == nil { + res = append(res, meters.MeasurementResult{ + Measurement: meters.BatteryCurrent, + Value: value, + Timestamp: timestamp, + }) + } + + // Power (0x74) - Negated to match the Perl script + if value, err := readSEFloat(batteryData, 16); err == nil { + res = append(res, meters.MeasurementResult{ + Measurement: meters.BatteryPower, + Value: -value, // Negate the value like the Perl script does + Timestamp: timestamp, + }) + } + } + + // Third chunk: Read the energy, SOH, SOC registers + energyData, err := client.ReadHoldingRegisters(baseAddr + 0x80, 6) + if err == nil && len(energyData) >= 12 { + // Available Energy (0x80) + if value, err := readSEFloat(energyData, 0); err == nil { + res = append(res, meters.MeasurementResult{ + Measurement: meters.BatteryAvailableEnergy, + Value: value * 0.001, // Convert Wh to kWh + Timestamp: timestamp, + }) + } + + // State of Health (0x82) + if value, err := readSEFloat(energyData, 4); err == nil { + res = append(res, meters.MeasurementResult{ + Measurement: meters.BatterySOH, + Value: value, + Timestamp: timestamp, + }) + } + + // State of Energy (0x84) + if value, err := readSEFloat(energyData, 8); err == nil { + res = append(res, meters.MeasurementResult{ + Measurement: meters.BatterySOC, + Value: value, + Timestamp: timestamp, + }) + } + } + + return res, nil +} diff --git a/meters/sunspec/sunspec.go b/meters/sunspec/sunspec.go index e2071569..4531b19b 100644 --- a/meters/sunspec/sunspec.go +++ b/meters/sunspec/sunspec.go @@ -59,19 +59,38 @@ func DeviceTree(client modbus.Client) ([]sunspec.Device, error) { } // NewDevice creates a Sunspec device -func NewDevice(meterType string, subdevice ...int) *SunSpec { - var dev int - if len(subdevice) > 0 { - dev = subdevice[0] - } - - return &SunSpec{ - subdevice: dev, - descriptor: meters.DeviceDescriptor{ - Type: meterType, - Manufacturer: meterType, - SubDevice: dev, - }, +func NewDevice(meterType string, subdevice int) meters.Device { + // Handle special device types + meterType = strings.ToUpper(meterType) + + // Special device types + switch meterType { + case "SE-BAT", "SOLAREDGE-BAT", "SE-BATTERY", "SOLAREDGE-BATTERY": + return NewSolarEdgeBatteryDevice(subdevice) + case "FRONIUS": + return NewFroniusDevice(subdevice) + case "KOSTAL": + return NewKostalDevice(subdevice) + case "KACO": + return NewKacoDevice(subdevice) + case "SE", "SOLAREDGE": + return NewSolarEdgeDevice(subdevice) + case "SMA": + return NewSMADevice(subdevice) + case "STECA": + return NewStecaDevice(subdevice) + case "SUNS", "SUNSPEC": + return NewSunSpecDevice(subdevice) + default: + // Default SunSpec device + return &SunSpec{ + subdevice: subdevice, + descriptor: meters.DeviceDescriptor{ + Type: meterType, + Manufacturer: meterType, + SubDevice: subdevice, + }, + } } } @@ -374,3 +393,97 @@ func (d *SunSpec) Query(client modbus.Client) (res []meters.MeasurementResult, e return res, nil } + +// Add the required device constructors for supported devices +// These were referenced in the NewDevice function + +// NewFroniusDevice creates a Fronius device +func NewFroniusDevice(subdevice int) *SunSpec { + return &SunSpec{ + subdevice: subdevice, + descriptor: meters.DeviceDescriptor{ + Type: "FRONIUS", + Manufacturer: "Fronius", + Model: "Generic", + SubDevice: subdevice, + }, + } +} + +// NewKostalDevice creates a Kostal device +func NewKostalDevice(subdevice int) *SunSpec { + return &SunSpec{ + subdevice: subdevice, + descriptor: meters.DeviceDescriptor{ + Type: "KOSTAL", + Manufacturer: "Kostal", + Model: "Generic", + SubDevice: subdevice, + }, + } +} + +// NewKacoDevice creates a Kaco device +func NewKacoDevice(subdevice int) *SunSpec { + return &SunSpec{ + subdevice: subdevice, + descriptor: meters.DeviceDescriptor{ + Type: "KACO", + Manufacturer: "Kaco", + Model: "Generic", + SubDevice: subdevice, + }, + } +} + +// NewSolarEdgeDevice creates a SolarEdge device +func NewSolarEdgeDevice(subdevice int) *SunSpec { + return &SunSpec{ + subdevice: subdevice, + descriptor: meters.DeviceDescriptor{ + Type: "SOLAREDGE", + Manufacturer: "SolarEdge", + Model: "Generic", + SubDevice: subdevice, + }, + } +} + +// NewSMADevice creates a SMA device +func NewSMADevice(subdevice int) *SunSpec { + return &SunSpec{ + subdevice: subdevice, + descriptor: meters.DeviceDescriptor{ + Type: "SMA", + Manufacturer: "SMA", + Model: "Generic", + SubDevice: subdevice, + }, + } +} + +// NewStecaDevice creates a Steca device +func NewStecaDevice(subdevice int) *SunSpec { + return &SunSpec{ + subdevice: subdevice, + descriptor: meters.DeviceDescriptor{ + Type: "STECA", + Manufacturer: "Steca", + Model: "Generic", + SubDevice: subdevice, + }, + } +} + +// NewSunSpecDevice creates a generic SunSpec device +func NewSunSpecDevice(subdevice int) *SunSpec { + return &SunSpec{ + subdevice: subdevice, + descriptor: meters.DeviceDescriptor{ + Type: "SUNSPEC", + Manufacturer: "SunSpec", + Model: "Generic", + SubDevice: subdevice, + }, + } +}