diff --git a/charger/mypv-elwa2.go b/charger/mypv-elwa2.go new file mode 100644 index 000000000..82c389333 --- /dev/null +++ b/charger/mypv-elwa2.go @@ -0,0 +1,214 @@ +package charger + +// LICENSE + +// Copyright (c) 2024 andig + +// This module is NOT covered by the MIT license. All rights reserved. + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import ( + "encoding/binary" + "sync/atomic" + "time" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/modbus" + "github.com/evcc-io/evcc/util/sponsor" +) + +// MyPvElwa2 charger implementation +type MyPvElwa2 struct { + log *util.Logger + conn *modbus.Connection + power uint32 +} + +const ( + elwaRegSetPower = 1000 + elwaRegTemp = 1001 + elwaRegTempLimit = 1002 + elwaRegStatus = 1003 + elwaRegPower = 1074 +) + +func init() { + registry.Add("ac-elwa-2", NewMyPvElwa2FromConfig) +} + +// https://github.com/evcc-io/evcc/discussions/12761 + +// NewMyPvElwa2FromConfig creates a MyPvElwa2 charger from generic config +func NewMyPvElwa2FromConfig(other map[string]interface{}) (api.Charger, error) { + cc := modbus.TcpSettings{ + ID: 1, + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + return NewMyPvElwa2(cc.URI, cc.ID) +} + +// NewMyPvElwa2 creates myPV AC Elwa 2 charger +func NewMyPvElwa2(uri string, slaveID uint8) (api.Charger, error) { + conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, slaveID) + if err != nil { + return nil, err + } + + if !sponsor.IsAuthorized() { + return nil, api.ErrSponsorRequired + } + + log := util.NewLogger("ac-elwa-2") + conn.Logger(log.TRACE) + + wb := &MyPvElwa2{ + log: log, + conn: conn, + } + + go wb.heartbeat(30 * time.Second) + + return wb, nil +} + +var _ api.IconDescriber = (*MyPvElwa2)(nil) + +// Icon implements the api.IconDescriber interface +func (v *MyPvElwa2) Icon() string { + return "waterheater" +} + +var _ api.FeatureDescriber = (*MyPvElwa2)(nil) + +// Features implements the api.FeatureDescriber interface +func (wb *MyPvElwa2) Features() []api.Feature { + return []api.Feature{api.IntegratedDevice, api.Heating} +} + +func (wb *MyPvElwa2) heartbeat(timeout time.Duration) { + for range time.Tick(timeout) { + if power := uint16(atomic.LoadUint32(&wb.power)); power > 0 { + enabled, err := wb.Enabled() + if err == nil && enabled { + err = wb.setPower(power) + } + if err != nil { + wb.log.ERROR.Println("heartbeat:", err) + } + } + } +} + +// Status implements the api.Charger interface +func (wb *MyPvElwa2) Status() (api.ChargeStatus, error) { + res := api.StatusA + b, err := wb.conn.ReadInputRegisters(elwaRegStatus, 1) + if err != nil { + return res, err + } + + res = api.StatusB + if binary.BigEndian.Uint16(b) == 2 { + res = api.StatusC + } + + return res, nil +} + +// Enabled implements the api.Charger interface +func (wb *MyPvElwa2) Enabled() (bool, error) { + b, err := wb.conn.ReadHoldingRegisters(elwaRegSetPower, 1) + if err != nil { + return false, err + } + + return binary.BigEndian.Uint16(b) > 0, nil +} + +func (wb *MyPvElwa2) setPower(power uint16) error { + b := make([]byte, 2) + binary.BigEndian.PutUint16(b, power) + + _, err := wb.conn.WriteMultipleRegisters(elwaRegSetPower, 1, b) + return err +} + +// Enable implements the api.Charger interface +func (wb *MyPvElwa2) Enable(enable bool) error { + var power uint16 + if enable { + power = uint16(atomic.LoadUint32(&wb.power)) + } + + return wb.setPower(power) +} + +// MaxCurrent implements the api.Charger interface +func (wb *MyPvElwa2) MaxCurrent(current int64) error { + return wb.MaxCurrentMillis(float64(current)) +} + +var _ api.ChargerEx = (*MyPvElwa2)(nil) + +// MaxCurrentMillis implements the api.ChargerEx interface +func (wb *MyPvElwa2) MaxCurrentMillis(current float64) error { + power := uint16(230 * current) + + err := wb.setPower(power) + if err == nil { + atomic.StoreUint32(&wb.power, uint32(power)) + } + + return err +} + +var _ api.Meter = (*MyPvElwa2)(nil) + +// CurrentPower implements the api.Meter interface +func (wb *MyPvElwa2) CurrentPower() (float64, error) { + b, err := wb.conn.ReadInputRegisters(elwaRegPower, 1) + if err != nil { + return 0, err + } + + return float64(binary.BigEndian.Uint16(b)), nil +} + +var _ api.Battery = (*MyPvElwa2)(nil) + +// CurrentPower implements the api.Meter interface +func (wb *MyPvElwa2) Soc() (float64, error) { + b, err := wb.conn.ReadInputRegisters(elwaRegTemp, 1) + if err != nil { + return 0, err + } + + return float64(binary.BigEndian.Uint16(b)) / 10, nil +} + +var _ api.SocLimiter = (*MyPvElwa2)(nil) + +// GetLimitSoc implements the api.SocLimiter interface +func (wb *MyPvElwa2) GetLimitSoc() (int64, error) { + b, err := wb.conn.ReadInputRegisters(elwaRegTempLimit, 1) + if err != nil { + return 0, err + } + + return int64(binary.BigEndian.Uint16(b)) / 10, nil +} diff --git a/templates/definition/charger/ac-elwa-2.yaml b/templates/definition/charger/ac-elwa-2.yaml index 8d9060ee6..1806f1eb7 100644 --- a/templates/definition/charger/ac-elwa-2.yaml +++ b/templates/definition/charger/ac-elwa-2.yaml @@ -7,54 +7,4 @@ params: - name: modbus choice: ["tcpip"] render: | - type: custom - enable: - source: modbus - {{- include "modbus" . | indent 2 }} - register: - address: 1000 - type: writeholding - decode: uint16 - enabled: - source: modbus - {{- include "modbus" . | indent 2 }} - register: - address: 1000 - type: holding - decode: uint16 - maxcurrent: - source: go - script: | - 230 * maxcurrent - out: - - name: power - type: int - config: - source: modbus - {{- include "modbus" . | indent 8 }} - register: - address: 1000 - type: writeholding - decode: uint16 - status: - source: combined - plugged: - source: const - value: 1 - charging: - source: modbus - {{- include "modbus" . | indent 4 }} - register: - address: 1074 # alternativ 1003 mit bit pattern - type: holding - decode: uint16 - soc: - source: modbus - {{- include "modbus" . | indent 2 }} - register: - address: 1001 - type: holding - decode: uint16 - scale: 0.1 - features: ["integrateddevice", "heating"] - icon: waterheater + {{- include "modbus" . }}