evcc-io/charger/vestel.go

365 lines
10 KiB
Go

package charger
// LICENSE
// Copyright (c) evcc.io (andig, naltatis, 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 (
"context"
"encoding/binary"
"fmt"
"strings"
"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"
"github.com/evcc-io/evcc/util/sponsor"
"github.com/hashicorp/go-version"
)
const (
vestelRegSerial = 100 // 25
vestelRegBrand = 190 // 10
vestelRegModel = 210 // 5
vestelRegFirmware = 230 // 50
vestelRegNumberPhases = 404
vestelRegPhasesSwitch = 405
vestelRegChargeStatus = 1001
vestelRegCableStatus = 1004
vestelRegChargeTime = 1508
vestelRegMaxCurrent = 5004
vestelRegPower = 1020
vestelRegTotalEnergy = 1036
vestelRegSessionEnergy = 1502
vestelRegRFID = 1516
vestelRegFailsafeTimeout = 2002
vestelRegAlive = 6000
// vestelRegChargepointState = 1000
)
var (
vestelRegCurrents = []uint16{1008, 1010, 1012} // non-continuous uint16 registers!
vestelRegVoltages = []uint16{1014, 1016, 1018} // non-continuous uint16 registers!
)
// Vestel is an api.Charger implementation for Vestel/Hymes wallboxes with Ethernet (SW modells).
// It uses Modbus TCP to communicate with the wallbox at modbus client id 255.
type Vestel struct {
implement.Caps
log *util.Logger
conn *modbus.Connection
enabled bool
current uint16
}
func init() {
registry.AddCtx("vestel", NewVestelFromConfig)
}
// NewVestelFromConfig creates a Vestel charger from generic config
func NewVestelFromConfig(ctx context.Context, other map[string]any) (api.Charger, error) {
cc := modbus.TcpSettings{
ID: 255,
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
return NewVestel(ctx, cc)
}
// NewVestel creates a Vestel charger
func NewVestel(ctx context.Context, settings modbus.TcpSettings) (api.Charger, error) {
conn, err := settings.Connection(ctx)
if err != nil {
return nil, err
}
if !sponsor.IsAuthorized() {
return nil, api.ErrSponsorRequired
}
log := util.NewLogger("vestel")
conn.Logger(log.TRACE)
wb := &Vestel{
Caps: implement.New(),
log: log,
conn: conn,
current: 6,
}
if b, err := wb.conn.ReadInputRegisters(vestelRegNumberPhases, 1); err == nil && binary.BigEndian.Uint16(b) == 1 {
implement.Has(wb, implement.PhaseSwitcher(wb.phases1p3p))
implement.Has(wb, implement.PhaseGetter(wb.getPhases))
}
// compare firmware version to determine if RFID is available
b, err := wb.conn.ReadInputRegisters(vestelRegFirmware, 50)
if err != nil {
return nil, fmt.Errorf("failed to read firmware version: %w", err)
}
fw, err := utf16BEBytesAsString(b)
if err == nil {
fw, _, _ = strings.Cut(strings.TrimPrefix(fw, "v"), "-")
if v, err := version.NewSemver(fw); err == nil {
if v.GreaterThanOrEqual(version.Must(version.NewSemver("3.156.0"))) {
// firmware >= v3.156.0 supports RFID according to https://github.com/evcc-io/evcc/issues/21359
implement.Has(wb, implement.Identifier(wb.identify))
}
} else {
log.WARN.Printf("failed to parse firmware version %q: %v", string(b), err)
}
} else {
log.WARN.Printf("failed to decode firmware version %q: %v", b, err)
}
// get failsafe timeout from charger
b, err = wb.conn.ReadHoldingRegisters(vestelRegFailsafeTimeout, 1)
if err != nil {
return nil, fmt.Errorf("failsafe timeout: %w", err)
}
timeout := 5 * time.Second // 20s/4
if u := binary.BigEndian.Uint16(b); u > 0 {
timeout = time.Duration(u) * time.Second / 4
}
if timeout < time.Second {
timeout = time.Second
}
go wb.heartbeat(ctx, timeout)
return wb, nil
}
func (wb *Vestel) heartbeat(ctx context.Context, timeout time.Duration) {
for tick := time.Tick(timeout); ; {
select {
case <-tick:
case <-ctx.Done():
return
}
if _, err := wb.conn.WriteSingleRegister(vestelRegAlive, 1); err != nil {
wb.log.ERROR.Println("heartbeat:", err)
}
}
}
// Status implements the api.Charger interface
func (wb *Vestel) Status() (api.ChargeStatus, error) {
res := api.StatusA
b, err := wb.conn.ReadInputRegisters(vestelRegCableStatus, 1)
if err == nil && binary.BigEndian.Uint16(b) >= 2 {
res = api.StatusB
b, err = wb.conn.ReadInputRegisters(vestelRegChargeStatus, 1)
if err == nil && binary.BigEndian.Uint16(b) == 1 {
res = api.StatusC
}
}
return res, err
}
// Enabled implements the api.Charger interface
func (wb *Vestel) Enabled() (bool, error) {
return verifyEnabled(wb, wb.enabled)
}
// Enable implements the api.Charger interface
func (wb *Vestel) Enable(enable bool) error {
var u uint16
if enable {
u = wb.current
}
_, err := wb.conn.WriteSingleRegister(vestelRegMaxCurrent, u)
if err == nil {
wb.enabled = enable
}
return err
}
// MaxCurrent implements the api.Charger interface
func (wb *Vestel) MaxCurrent(current int64) error {
if current < 6 {
return fmt.Errorf("invalid current %d", current)
}
u := uint16(current)
_, err := wb.conn.WriteSingleRegister(vestelRegMaxCurrent, u)
if err == nil {
wb.current = u
}
return err
}
var _ api.CurrentGetter = (*Vestel)(nil)
// GetMaxCurrent implements the api.CurrentGetter interface
func (wb *Vestel) GetMaxCurrent() (float64, error) {
b, err := wb.conn.ReadHoldingRegisters(vestelRegMaxCurrent, 1)
if err != nil {
return 0, err
}
return float64(binary.BigEndian.Uint16(b)), nil
}
var _ api.ChargeTimer = (*Vestel)(nil)
// ChargeDuration implements the api.ChargeTimer interface
func (wb *Vestel) ChargeDuration() (time.Duration, error) {
b, err := wb.conn.ReadInputRegisters(vestelRegChargeTime, 2)
if err != nil {
return 0, err
}
return time.Duration(binary.BigEndian.Uint32(b)) * time.Second, nil
}
var _ api.Meter = (*Vestel)(nil)
// CurrentPower implements the api.Meter interface
func (wb *Vestel) CurrentPower() (float64, error) {
b, err := wb.conn.ReadInputRegisters(vestelRegPower, 2)
if err != nil {
return 0, err
}
return float64(binary.BigEndian.Uint32(b)), err
}
var _ api.MeterEnergy = (*Vestel)(nil)
// TotalEnergy implements the api.MeterEnergy interface
func (wb *Vestel) TotalEnergy() (float64, error) {
b, err := wb.conn.ReadInputRegisters(vestelRegTotalEnergy, 2)
if err != nil {
return 0, err
}
return float64(binary.BigEndian.Uint32(b)) / 10, err
}
var _ api.ChargeRater = (*Vestel)(nil)
// ChargedEnergy implements the api.ChargeRater interface
func (wb *Vestel) ChargedEnergy() (float64, error) {
b, err := wb.conn.ReadInputRegisters(vestelRegSessionEnergy, 2)
if err != nil {
return 0, err
}
return float64(binary.BigEndian.Uint32(b)) / 1e3, err
}
// getPhaseValues returns 3 sequential register values
func (wb *Vestel) getPhaseValues(regs []uint16, divider float64) (float64, float64, float64, error) {
var res [3]float64
for i, reg := range regs {
b, err := wb.conn.ReadInputRegisters(reg, 1)
if err != nil {
return 0, 0, 0, err
}
res[i] = float64(binary.BigEndian.Uint16(b)) / divider
}
return res[0], res[1], res[2], nil
}
var _ api.PhaseCurrents = (*Vestel)(nil)
// Currents implements the api.PhaseCurrents interface
func (wb *Vestel) Currents() (float64, float64, float64, error) {
return wb.getPhaseValues(vestelRegCurrents, 1e3)
}
var _ api.PhaseVoltages = (*Vestel)(nil)
// Voltages implements the api.PhaseVoltages interface
func (wb *Vestel) Voltages() (float64, float64, float64, error) {
return wb.getPhaseValues(vestelRegVoltages, 1)
}
// phases1p3p implements the api.PhaseSwitcher interface
func (wb *Vestel) phases1p3p(phases int) error {
_, err := wb.conn.WriteSingleRegister(vestelRegPhasesSwitch, uint16((phases-1)>>1))
return err
}
// getPhases implements the api.PhaseGetter interface
func (wb *Vestel) getPhases() (int, error) {
b, err := wb.conn.ReadHoldingRegisters(vestelRegPhasesSwitch, 1)
if err != nil {
return 0, err
}
return 1 + int(binary.BigEndian.Uint16(b))<<1, nil
}
// Identify implements the api.Identifier interface
func (wb *Vestel) identify() ([]string, error) {
b, err := wb.conn.ReadInputRegisters(vestelRegRFID, 15)
if err != nil {
return nil, err
}
id, err := utf16BEBytesAsString(b)
return []string{id}, err
}
var _ api.Diagnosis = (*Vestel)(nil)
// Diagnose implements the api.Diagnosis interface
func (wb *Vestel) Diagnose() {
if b, err := wb.conn.ReadInputRegisters(vestelRegBrand, 10); err == nil {
s, _ := utf16BEBytesAsString(b)
fmt.Printf("Brand:\t%s\n", s)
}
if b, err := wb.conn.ReadInputRegisters(vestelRegModel, 5); err == nil {
s, _ := utf16BEBytesAsString(b)
fmt.Printf("Model:\t%s\n", s)
}
if b, err := wb.conn.ReadInputRegisters(vestelRegSerial, 25); err == nil {
s, _ := utf16BEBytesAsString(b)
fmt.Printf("Serial:\t%s\n", s)
}
if b, err := wb.conn.ReadInputRegisters(vestelRegFirmware, 50); err == nil {
s, _ := utf16BEBytesAsString(b)
fmt.Printf("Firmware:\t%s\n", s)
}
if b, err := wb.conn.ReadHoldingRegisters(vestelRegFailsafeTimeout, 1); err == nil {
fmt.Printf("Failsafe timeout:\t%#x\n", binary.BigEndian.Uint16(b))
}
if b, err := wb.conn.ReadInputRegisters(vestelRegNumberPhases, 1); err == nil {
fmt.Printf("Number of phases:\t%#x\n", binary.BigEndian.Uint16(b))
}
if b, err := wb.conn.ReadHoldingRegisters(vestelRegPhasesSwitch, 1); err == nil {
fmt.Printf("Phase switch:\t%#x\n", binary.BigEndian.Uint16(b))
}
if b, err := wb.conn.ReadInputRegisters(vestelRegRFID, 15); err == nil {
s, _ := utf16BEBytesAsString(b)
fmt.Printf("RFID:\t%s\n", s)
}
}