From 56a2b8db58f9f44007a29d9008adcf7f0e46dff0 Mon Sep 17 00:00:00 2001 From: motze92 <41009610+motze92@users.noreply.github.com> Date: Fri, 26 Jan 2024 15:58:05 +0100 Subject: [PATCH] Add GoodWe over Wifi (#11750) --- Dockerfile | 2 + meter/goodwe-wifi.go | 73 +++++++++++++ meter/goodwe-wifi_decorators.go | 35 +++++++ meter/goodwe/server.go | 109 ++++++++++++++++++++ meter/goodwe/types.go | 18 ++++ templates/definition/meter/goodwe-wifi.yaml | 17 +++ 6 files changed, 254 insertions(+) create mode 100644 meter/goodwe-wifi.go create mode 100644 meter/goodwe-wifi_decorators.go create mode 100644 meter/goodwe/server.go create mode 100644 meter/goodwe/types.go create mode 100644 templates/definition/meter/goodwe-wifi.yaml diff --git a/Dockerfile b/Dockerfile index da7ca9032..6715c006d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -90,6 +90,8 @@ EXPOSE 7070/tcp EXPOSE 7090/udp # OCPP charger EXPOSE 8887/tcp +# GoodWe Wifi Inverter +EXPOSE 8899/udp # SMA Energy Manager EXPOSE 9522/udp diff --git a/meter/goodwe-wifi.go b/meter/goodwe-wifi.go new file mode 100644 index 000000000..26652e21c --- /dev/null +++ b/meter/goodwe-wifi.go @@ -0,0 +1,73 @@ +package meter + +import ( + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/meter/goodwe" + "github.com/evcc-io/evcc/util" +) + +type goodWeWiFi struct { + *goodwe.Server + usage string + uri string +} + +func init() { + registry.Add("goodwe-wifi", NewGoodWeWifiFromConfig) +} + +//go:generate go run ../cmd/tools/decorate.go -f decorateGoodWeWifi -b *goodWeWiFi -r api.Meter -t "api.Battery,Soc,func() (float64, error)" + +func NewGoodWeWifiFromConfig(other map[string]interface{}) (api.Meter, error) { + var cc struct { + capacity `mapstructure:",squash"` + URI, Usage string + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + return NewGoodWeWiFi(cc.URI, cc.Usage) +} + +func NewGoodWeWiFi(uri string, usage string) (api.Meter, error) { + instance, err := goodwe.Instance() + if err != nil { + return nil, err + } + + res := &goodWeWiFi{ + Server: instance, + usage: usage, + uri: uri, + } + + instance.AddInverter(uri) + + // decorate api.BatterySoc + var batterySoc func() (float64, error) + if usage == "battery" { + batterySoc = res.batterySoc + } + + return decorateGoodWeWifi(res, batterySoc), nil +} + +func (m *goodWeWiFi) CurrentPower() (float64, error) { + data := m.GetInverter(m.uri) + + switch m.usage { + case "grid": + return data.NetPower, nil + case "pv": + return data.PvPower, nil + case "battery": + return data.BatteryPower, nil + } + return 0, api.ErrNotAvailable +} + +func (m *goodWeWiFi) batterySoc() (float64, error) { + return m.GetInverter(m.uri).Soc, nil +} diff --git a/meter/goodwe-wifi_decorators.go b/meter/goodwe-wifi_decorators.go new file mode 100644 index 000000000..e44f0d9bd --- /dev/null +++ b/meter/goodwe-wifi_decorators.go @@ -0,0 +1,35 @@ +package meter + +// Code generated by github.com/evcc-io/evcc/cmd/tools/decorate.go. DO NOT EDIT. + +import ( + "github.com/evcc-io/evcc/api" +) + +func decorateGoodWeWifi(base *goodWeWiFi, battery func() (float64, error)) api.Meter { + switch { + case battery == nil: + return base + + case battery != nil: + return &struct { + *goodWeWiFi + api.Battery + }{ + goodWeWiFi: base, + Battery: &decorateGoodWeWifiBatteryImpl{ + battery: battery, + }, + } + } + + return nil +} + +type decorateGoodWeWifiBatteryImpl struct { + battery func() (float64, error) +} + +func (impl *decorateGoodWeWifiBatteryImpl) Soc() (float64, error) { + return impl.battery() +} diff --git a/meter/goodwe/server.go b/meter/goodwe/server.go new file mode 100644 index 000000000..9a847c3ac --- /dev/null +++ b/meter/goodwe/server.go @@ -0,0 +1,109 @@ +package goodwe + +import ( + "encoding/binary" + "net" + "sync" + "time" +) + +var ( + instance *Server + mu sync.RWMutex +) + +func Instance() (*Server, error) { + mu.Lock() + defer mu.Unlock() + + if instance != nil { + return instance, nil + } + + instance = &Server{ + inverters: make(map[string]Inverter), + } + + addr, err := net.ResolveUDPAddr("udp", "0.0.0.0:8899") + if err != nil { + return nil, err + } + + instance.conn, err = net.ListenUDP("udp", addr) + if err != nil { + return nil, err + } + + go instance.listen() + go instance.readData() + + return instance, err +} + +func (m *Server) AddInverter(ip string) { + mu.Lock() + defer mu.Unlock() + m.inverters[ip] = Inverter{IP: ip} +} + +func (m *Server) GetInverter(uri string) Inverter { + mu.RLock() + defer mu.RUnlock() + return m.inverters[uri] +} + +func (m *Server) readData() { + for _, inverter := range m.inverters { + addr, err := net.ResolveUDPAddr("udp", inverter.IP+":8899") + if err != nil { + return + } + + if _, err := m.conn.WriteToUDP([]byte{0xF7, 0x03, 0x89, 0x1C, 0x00, 0x7D, 0x7A, 0xE7}, addr); err != nil { + return + } + time.Sleep(5 * time.Second) + if _, err := m.conn.WriteToUDP([]byte{0xF7, 0x03, 0x90, 0x88, 0x00, 0x0D, 0x3D, 0xB3}, addr); err != nil { + return + } + } + m.readData() +} + +func (m *Server) listen() { + for { + buf := make([]byte, 1024) + _, addr, err := m.conn.ReadFromUDP(buf) + if err != nil { + continue + } + + ip := addr.IP.String() + + mu.Lock() + if buf[4] == 250 { + vPv1 := float64(int16(binary.BigEndian.Uint16(buf[11:]))) * 0.1 + vPv2 := float64(int16(binary.BigEndian.Uint16(buf[19:]))) * 0.1 + iPv1 := float64(int16(binary.BigEndian.Uint16(buf[13:]))) * 0.1 + iPv2 := float64(int16(binary.BigEndian.Uint16(buf[21:]))) * 0.1 + iBatt := float64(int16(binary.BigEndian.Uint16(buf[167:]))) * 0.1 + vBatt := float64(int16(binary.BigEndian.Uint16(buf[165:]))) * 0.1 + + pvPower := vPv1*iPv1 + vPv2*iPv2 + + inverter := m.inverters[ip] + inverter.PvPower = pvPower + inverter.BatteryPower = vBatt * iBatt + inverter.NetPower = -float64(int32(binary.BigEndian.Uint32(buf[83:]))) + + m.inverters[ip] = inverter + } + + if buf[4] == 26 { + inverter := m.inverters[ip] + inverter.Soc = float64(buf[20]) + m.inverters[ip] = inverter + } + mu.Unlock() + } +} diff --git a/meter/goodwe/types.go b/meter/goodwe/types.go new file mode 100644 index 000000000..06bb99862 --- /dev/null +++ b/meter/goodwe/types.go @@ -0,0 +1,18 @@ +package goodwe + +import ( + "net" +) + +type Server struct { + conn *net.UDPConn + inverters map[string]Inverter +} + +type Inverter struct { + IP string + PvPower float64 + NetPower float64 + BatteryPower float64 + Soc float64 +} diff --git a/templates/definition/meter/goodwe-wifi.yaml b/templates/definition/meter/goodwe-wifi.yaml new file mode 100644 index 000000000..093637dc6 --- /dev/null +++ b/templates/definition/meter/goodwe-wifi.yaml @@ -0,0 +1,17 @@ +template: goodwe-wifi +products: + - brand: GoodWe + description: + generic: GoodWe over Wifi +params: + - name: usage + choice: ["grid", "pv", "battery"] + allinone: true + - name: uri + description: + en: IP address of the GoodWe inverter. + de: IP-Adresse des GoodWe Wechselrichters. +render: | + type: goodwe-wifi + usage: {{ .usage }} + uri: {{ .uri }}