From d69077b42390727e1e0fa584b1d5df50556bfbac Mon Sep 17 00:00:00 2001 From: premultiply <4681172+premultiply@users.noreply.github.com> Date: Wed, 1 Mar 2023 21:20:43 +0100 Subject: [PATCH] Add Hesotec chargers (#6378) --- charger/hesotec.go | 256 ++++++++++++++++++++++ templates/definition/charger/hesotec.yaml | 13 ++ templates/docs/charger/hesotec_0.yaml | 14 ++ templates/evcc.io/brands.json | 1 + 4 files changed, 284 insertions(+) create mode 100644 charger/hesotec.go create mode 100644 templates/definition/charger/hesotec.yaml create mode 100644 templates/docs/charger/hesotec_0.yaml diff --git a/charger/hesotec.go b/charger/hesotec.go new file mode 100644 index 000000000..bc68c409d --- /dev/null +++ b/charger/hesotec.go @@ -0,0 +1,256 @@ +package charger + +// LICENSE + +// Copyright (c) 2023 premultiply + +// 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" + "fmt" + "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" +) + +// Hesotec charger implementation +type Hesotec struct { + conn *modbus.Connection + curr uint16 +} + +const ( + hesotecRegFirmware1 = 0x0010 // R Firmware Version eSat Control 3 - ASCII + hesotecRegFirmware2 = 0x0013 // R Firmware Version eSat Power 3 - ASCII + hesotecRegFirmware3 = 0x0016 // R Firmware Version eSat Oak 3 - ASCII + hesotecRegFirmware4 = 0x0019 // R Firmware Version eSat Display 3 - ASCII + hesotecRegFirmware5 = 0x001C // R Firmware Version ESP 3 - ASCII + hesotecRegSessAuth = 0x1000 // RW B_Autorisierung 1 - u16 + hesotecRegSessStop = 0x1001 // RW B_Stop_RFID 1 - u16 + hesotecRegSessPause = 0x1002 // RW B_Pause 1 - u16 + hesotecRegCurrent = 0x1003 // RW I_Strom_Max_Last 1 A u16 + hesotecRegTemp = 0x4000 // R N_Temperatur 1 °C i16 + hesotecRegVoltages = 0x4001 // R N_Spannung_1 1 V u16 + hesotecRegCurrents = 0x4004 // R N_Strom_1 2 mA u32 + hesotecRegPower = 0x400A // R N_Wirkleistung 2 mW u32 + hesotecRegCurrCP = 0x4016 // R I_Strom_CP 2 mA u32 + hesotecRegStatus = 0x4018 // R E_Status_CP 1 - ASCII + hesotecRegDuration = 0x401A // R N_Dauer_Ladesitzung 2 s u32 + hesotecRegEnergy = 0x401C // R N_Energie_Ladesitzung 2 Wh u32 +) + +func init() { + registry.Add("hesotec", NewHesotecFromConfig) +} + +// NewHesotecFromConfig creates a Hesotec charger from generic config +func NewHesotecFromConfig(other map[string]interface{}) (api.Charger, error) { + cc := modbus.TcpSettings{ + ID: 1, + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + return NewHesotec(cc.URI, cc.ID) +} + +// NewHesotec creates Hesotec charger +func NewHesotec(uri string, id uint8) (api.Charger, error) { + conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, id) + if err != nil { + return nil, err + } + + if !sponsor.IsAuthorized() { + return nil, api.ErrSponsorRequired + } + + log := util.NewLogger("hesotec") + conn.Logger(log.TRACE) + + wb := &Hesotec{ + conn: conn, + curr: 6, // assume min current + } + + return wb, err +} + +// Status implements the api.Charger interface +func (wb *Hesotec) Status() (api.ChargeStatus, error) { + b, err := wb.conn.ReadHoldingRegisters(hesotecRegStatus, 1) + if err != nil { + return api.StatusNone, err + } + + switch s := string(b[0]); s { + case "A", "B", "C": + return api.ChargeStatus(s), nil + default: + return api.StatusNone, fmt.Errorf("invalid status: %s", s) + } +} + +// Enabled implements the api.Charger interface +func (wb *Hesotec) Enabled() (bool, error) { + b, err := wb.conn.ReadHoldingRegisters(hesotecRegSessPause, 1) + if err != nil { + return false, err + } + + return binary.BigEndian.Uint16(b) == 0, nil +} + +// Enable implements the api.Charger interface +func (wb *Hesotec) Enable(enable bool) error { + b := make([]byte, 2) + if !enable { + binary.BigEndian.PutUint16(b, 1) + } + + _, err := wb.conn.WriteMultipleRegisters(hesotecRegSessPause, 1, b) + + return err +} + +// MaxCurrent implements the api.Charger interface +func (wb *Hesotec) MaxCurrent(current int64) error { + if current < 6 { + return fmt.Errorf("invalid current %d", current) + } + + wb.curr = uint16(current) + + b := make([]byte, 2) + binary.BigEndian.PutUint16(b, wb.curr) + + _, err := wb.conn.WriteMultipleRegisters(hesotecRegCurrent, 1, b) + + return err +} + +var _ api.Meter = (*Hesotec)(nil) + +// CurrentPower implements the api.Meter interface +func (wb *Hesotec) CurrentPower() (float64, error) { + b, err := wb.conn.ReadHoldingRegisters(hesotecRegPower, 2) + if err != nil { + return 0, err + } + + return float64(binary.BigEndian.Uint32(b) * 1e3), nil +} + +var _ api.ChargeTimer = (*Hesotec)(nil) + +// ChargingTime implements the api.ChargeTimer interface +func (wb *Hesotec) ChargingTime() (time.Duration, error) { + b, err := wb.conn.ReadHoldingRegisters(hesotecRegDuration, 2) + if err != nil { + return 0, err + } + + return time.Duration(binary.BigEndian.Uint32(b)) * time.Second, nil +} + +var _ api.MeterEnergy = (*Hesotec)(nil) + +// TotalEnergy implements the api.MeterEnergy interface +func (wb *Hesotec) TotalEnergy() (float64, error) { + b, err := wb.conn.ReadHoldingRegisters(hesotecRegEnergy, 2) + if err != nil { + return 0, err + } + + return float64(binary.BigEndian.Uint32(b)) / 1e3, nil +} + +var _ api.PhaseCurrents = (*Hesotec)(nil) + +// Currents implements the api.PhaseCurrents interface +func (wb *Hesotec) Currents() (float64, float64, float64, error) { + b, err := wb.conn.ReadHoldingRegisters(hesotecRegCurrents, 6) + if err != nil { + return 0, 0, 0, err + } + + var curr [3]float64 + for l := 0; l < 3; l++ { + curr[l] = float64(binary.BigEndian.Uint32(b[4*l:])) + } + + return curr[0], curr[1], curr[2], nil +} + +var _ api.PhaseVoltages = (*Hesotec)(nil) + +// Voltages implements the api.PhaseVoltages interface +func (wb *Hesotec) Voltages() (float64, float64, float64, error) { + b, err := wb.conn.ReadHoldingRegisters(hesotecRegVoltages, 3) + if err != nil { + return 0, 0, 0, err + } + + var volt [3]float64 + for l := 0; l < 3; l++ { + volt[l] = float64(binary.BigEndian.Uint16(b[2*l:])) + } + + return volt[0], volt[1], volt[2], nil +} + +var _ api.Diagnosis = (*Hesotec)(nil) + +// Diagnose implements the api.Diagnosis interface +func (wb *Hesotec) Diagnose() { + if b, err := wb.conn.ReadHoldingRegisters(hesotecRegFirmware1, 3); err == nil { + fmt.Printf("\tFirmware Version eSat Control:\t%s\n", b) + } + if b, err := wb.conn.ReadHoldingRegisters(hesotecRegFirmware2, 3); err == nil { + fmt.Printf("\tFirmware Version eSat Power:\t%s\n", b) + } + if b, err := wb.conn.ReadHoldingRegisters(hesotecRegFirmware3, 3); err == nil { + fmt.Printf("\tFirmware Version eSat Oak:\t%s\n", b) + } + if b, err := wb.conn.ReadHoldingRegisters(hesotecRegFirmware4, 3); err == nil { + fmt.Printf("\tFirmware Version eSat Display:\t%s\n", b) + } + if b, err := wb.conn.ReadHoldingRegisters(hesotecRegFirmware5, 3); err == nil { + fmt.Printf("\tFirmware Version ESP:\t%s\n", b) + } + if b, err := wb.conn.ReadHoldingRegisters(hesotecRegSessAuth, 1); err == nil { + fmt.Printf("\tB_Autorisierung:\t%d\n", binary.BigEndian.Uint16(b)) + } + if b, err := wb.conn.ReadHoldingRegisters(hesotecRegSessStop, 1); err == nil { + fmt.Printf("\tB_Stop_RFID:\t%d\n", binary.BigEndian.Uint16(b)) + } + if b, err := wb.conn.ReadHoldingRegisters(hesotecRegSessPause, 1); err == nil { + fmt.Printf("\tB_Pause:\t%d\n", binary.BigEndian.Uint16(b)) + } + if b, err := wb.conn.ReadHoldingRegisters(hesotecRegCurrent, 1); err == nil { + fmt.Printf("\tI_Strom_Max_Last:\t%dA\n", binary.BigEndian.Uint16(b)) + } + if b, err := wb.conn.ReadHoldingRegisters(hesotecRegTemp, 1); err == nil { + fmt.Printf("\tN_Temperatur:\t%d°C\n", int16(binary.BigEndian.Uint16(b))) + } + if b, err := wb.conn.ReadHoldingRegisters(hesotecRegCurrCP, 2); err == nil { + fmt.Printf("\tI_Strom_CP:\t%dmA\n", binary.BigEndian.Uint32(b)) + } +} diff --git a/templates/definition/charger/hesotec.yaml b/templates/definition/charger/hesotec.yaml new file mode 100644 index 000000000..1c73cf687 --- /dev/null +++ b/templates/definition/charger/hesotec.yaml @@ -0,0 +1,13 @@ +template: hesotec +products: + - brand: Hesotec + description: + generic: eSat, eBox +requirements: + evcc: ["sponsorship"] +params: + - name: modbus + choice: ["tcpip"] +render: | + type: hesotec + {{- include "modbus" . }} diff --git a/templates/docs/charger/hesotec_0.yaml b/templates/docs/charger/hesotec_0.yaml new file mode 100644 index 000000000..d080f5a2e --- /dev/null +++ b/templates/docs/charger/hesotec_0.yaml @@ -0,0 +1,14 @@ +product: + brand: Hesotec + description: eSat, eBox +requirements: ["sponsorship"] +render: + - default: | + type: template + template: hesotec + + # Modbus TCP + modbus: tcpip + id: 1 + host: 192.0.2.2 # Hostname + port: 502 # Port diff --git a/templates/evcc.io/brands.json b/templates/evcc.io/brands.json index daa9f0483..3740a908b 100644 --- a/templates/evcc.io/brands.json +++ b/templates/evcc.io/brands.json @@ -21,6 +21,7 @@ "go-eCharger", "HardyBarth", "Heidelberg", + "Hesotec", "Innogy", "Juice", "KEBA",