Add Hoymiles HMT/HMS DTU PRO S Modbus TCP (#29281)
This commit is contained in:
parent
dc37e7a313
commit
315f94bde1
2 changed files with 166 additions and 0 deletions
136
meter/hoymiles-dtu-mbtcp.go
Normal file
136
meter/hoymiles-dtu-mbtcp.go
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
package meter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/api/implement"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/modbus"
|
||||
)
|
||||
|
||||
const (
|
||||
hoymilesPanelLengthBytes = uint16(0x28)
|
||||
hoymilesStartRegisterBytes = uint16(0x1000)
|
||||
hoymilesMaxPanels = 99
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.AddCtx("hoymiles-dtu-mb", NewHoymilesDTUModbusTcpFromConfig)
|
||||
}
|
||||
|
||||
type hoymilesDTUValues struct {
|
||||
power float64
|
||||
totalEnergy float64
|
||||
}
|
||||
|
||||
type hoymilesDTUModbusTCP struct {
|
||||
log *util.Logger
|
||||
conn *modbus.Connection
|
||||
valuesG func() (hoymilesDTUValues, error)
|
||||
}
|
||||
|
||||
// NewHoymilesDTUModbusTcpFromConfig creates a Hoymiles DTU meter from generic config
|
||||
func NewHoymilesDTUModbusTcpFromConfig(ctx context.Context, other map[string]any) (api.Meter, error) {
|
||||
cc := struct {
|
||||
modbus.TcpSettings `mapstructure:",squash"`
|
||||
pvMaxACPower `mapstructure:",squash"`
|
||||
Cache time.Duration
|
||||
}{
|
||||
TcpSettings: modbus.TcpSettings{ID: 1},
|
||||
Cache: 15 * time.Second,
|
||||
}
|
||||
|
||||
if err := util.DecodeOther(other, &cc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newHoymilesDTUModbusTCP(ctx, cc.URI, cc.ID, cc.pvMaxACPower.Decorator(), cc.Cache)
|
||||
}
|
||||
|
||||
func newHoymilesDTUModbusTCP(ctx context.Context, uri string, id uint8, maxACPower func() float64, cache time.Duration) (api.Meter, error) {
|
||||
conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log := util.NewLogger("hoymiles-dtu-mb")
|
||||
conn.Logger(log.TRACE)
|
||||
|
||||
m := &hoymilesDTUModbusTCP{
|
||||
log: log,
|
||||
conn: conn,
|
||||
}
|
||||
|
||||
m.valuesG = util.Cached(m.readCurrentValues, cache)
|
||||
|
||||
meter, _ := NewConfigurable(m.CurrentPower)
|
||||
implement.Has(meter, implement.MeterEnergy(m.TotalEnergy))
|
||||
implement.May(meter, implement.MaxACPowerGetter(maxACPower))
|
||||
|
||||
return meter, nil
|
||||
}
|
||||
|
||||
func (m *hoymilesDTUModbusTCP) panelPower(panelIndex int) (float64, float64, bool, error) {
|
||||
// Each panel's data is 0x28 bytes (40 bytes); panel N starts at register 0x1000 + N×0x28.
|
||||
// Confirmed with Hoymiles DTU PRO S hardware against the Modbus register map.
|
||||
startRegister := hoymilesStartRegisterBytes + uint16(panelIndex)*hoymilesPanelLengthBytes
|
||||
results, err := m.conn.ReadHoldingRegisters(startRegister, hoymilesPanelLengthBytes/2)
|
||||
if err != nil {
|
||||
return 0, 0, false, fmt.Errorf("failed to read panel %d: %w", panelIndex, err)
|
||||
}
|
||||
if len(results) < int(hoymilesPanelLengthBytes) {
|
||||
return 0, 0, false, fmt.Errorf("panel %d: short Modbus response: got %d bytes, expected %d", panelIndex, len(results), hoymilesPanelLengthBytes)
|
||||
}
|
||||
|
||||
// inverterSerial: bytes 0x01–0x06
|
||||
serialBytes := results[1:7]
|
||||
inverterSerial := fmt.Sprintf("%X", serialBytes)
|
||||
// all-zero serial means no more panels; DTU returns zeros for unoccupied slots
|
||||
serialIsZero := binary.BigEndian.Uint32(serialBytes[:4]) == 0 &&
|
||||
binary.BigEndian.Uint16(serialBytes[4:6]) == 0
|
||||
// portNumber: byte 0x07
|
||||
portNumber := int(results[7])
|
||||
if serialIsZero || portNumber == 0 {
|
||||
m.log.TRACE.Printf("panel %d: no more panels to read", panelIndex)
|
||||
return 0, 0, false, nil
|
||||
}
|
||||
// power: bytes 0x10–0x11, uint16 / 10 → watts (one decimal place precision)
|
||||
power := float64(binary.BigEndian.Uint16(results[16:18])) / 10.0
|
||||
// totalCumulativeProduction: bytes 0x14–0x17, uint32, in Wh
|
||||
totalCumulativeProduction := binary.BigEndian.Uint32(results[20:24])
|
||||
m.log.TRACE.Printf("panel %d: inverter serial %s, port %d, power %.1f W, cumulative production %d Wh", panelIndex, inverterSerial, portNumber, power, totalCumulativeProduction)
|
||||
return power, float64(totalCumulativeProduction), true, nil
|
||||
}
|
||||
|
||||
func (m *hoymilesDTUModbusTCP) readCurrentValues() (hoymilesDTUValues, error) {
|
||||
var values hoymilesDTUValues
|
||||
for i := 0; i < hoymilesMaxPanels; i++ {
|
||||
power, cumulative, found, err := m.panelPower(i)
|
||||
if err != nil {
|
||||
return hoymilesDTUValues{}, err
|
||||
}
|
||||
if !found {
|
||||
break
|
||||
}
|
||||
values.power += power
|
||||
values.totalEnergy += cumulative
|
||||
}
|
||||
m.log.DEBUG.Printf("total power from all panels: %.2f W, total cumulative production: %.2f Wh", values.power, values.totalEnergy)
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// CurrentPower implements the api.Meter interface
|
||||
func (m *hoymilesDTUModbusTCP) CurrentPower() (float64, error) {
|
||||
res, err := m.valuesG()
|
||||
return res.power, err
|
||||
}
|
||||
|
||||
// TotalEnergy implements the api.MeterEnergy interface
|
||||
func (m *hoymilesDTUModbusTCP) TotalEnergy() (float64, error) {
|
||||
res, err := m.valuesG()
|
||||
return res.totalEnergy / 1e3, err // Wh to kWh
|
||||
}
|
||||
30
templates/definition/meter/hoymiles-dtu-mb.yaml
Normal file
30
templates/definition/meter/hoymiles-dtu-mb.yaml
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
template: hoymiles-dtu-mb
|
||||
products:
|
||||
- brand: Hoymiles
|
||||
description:
|
||||
de: "HMT/HMS-Serie mit DTU PRO S / MODBUS TCP"
|
||||
en: "HMT/HMS-Series with DTU PRO S / MODBUS TCP"
|
||||
requirements:
|
||||
description:
|
||||
de: |
|
||||
Benötigt wird ein Hoymiles Wechselrichter der HMT/HMS Serie mit DTU PRO S datalogger mit Modbus TCP aktiviert (verbunden mit LAN)
|
||||
en: |
|
||||
Requires a Hoymiles Inverter of the HMT/HMS Series with DTU PRO S datalogger with Modbus TCP enabled (connected with LAN)
|
||||
params:
|
||||
- name: usage
|
||||
choice: ["pv"]
|
||||
- name: modbus
|
||||
choice: ["tcpip"]
|
||||
- name: maxacpower
|
||||
advanced: true
|
||||
default: "0"
|
||||
description:
|
||||
de: "Gesamtleistung der Wechselrichter"
|
||||
en: "Total maximum power of the inverters"
|
||||
help:
|
||||
de: "Gesamtleistung der Wechselrichter in Watt (z.B. 3000)"
|
||||
en: "Total maximum power of the inverters in watt (e.g. 3000)"
|
||||
render: |
|
||||
type: hoymiles-dtu-mb
|
||||
{{- include "modbus" . }}
|
||||
maxacpower: {{ .maxacpower }}
|
||||
Loading…
Add table
Add a link
Reference in a new issue