evcc-io/charger/twc3.go

236 lines
6.5 KiB
Go

package charger
import (
"errors"
"fmt"
"math"
"strings"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/core/loadpoint"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
)
// Twc3 is an api.Vehicle implementation for Twc3 cars
type Twc3 struct {
lp loadpoint.API
vitalsG func() (Vitals, error)
enabled bool
}
func init() {
registry.Add("twc3", NewTwc3FromConfig)
}
// Vitals is the /api/1/vitals response
type Vitals struct {
ContactorClosed bool `json:"contactor_closed"` // false
VehicleConnected bool `json:"vehicle_connected"` // false
SessionS int64 `json:"session_s"` // 0
GridV float64 `json:"grid_v"` // 230.1
GridHz float64 `json:"grid_hz"` // 49.928
VehicleCurrentA float64 `json:"vehicle_current_a"` // 0.1
CurrentAA float64 `json:"currentA_a"` // 0.0
CurrentBA float64 `json:"currentB_a"` // 0.1
CurrentCA float64 `json:"currentC_a"` // 0.0
CurrentNA float64 `json:"currentN_a"` // 0.0
VoltageAV float64 `json:"voltageA_v"` // 0.0
VoltageBV float64 `json:"voltageB_v"` // 0.0
VoltageCV float64 `json:"voltageC_v"` // 0.0
RelayCoilV float64 `json:"relay_coil_v"` // 11.8
PcbaTempC float64 `json:"pcba_temp_c"` // 19.2
HandleTempC float64 `json:"handle_temp_c"` // 15.3
McuTempC float64 `json:"mcu_temp_c"` // 25.1
UptimeS int `json:"uptime_s"` // 831580
InputThermopileUv float64 `json:"input_thermopile_uv"` //-233
ProxV float64 `json:"prox_v"` // 0.0
PilotHighV float64 `json:"pilot_high_v"` // 11.9
PilotLowV float64 `json:"pilot_low_v"` // 11.9
SessionEnergyWh float64 `json:"session_energy_wh"` // 22864.699
ConfigStatus int `json:"config_status"` // 5
EvseState int `json:"evse_state"` // 1
CurrentAlerts []any `json:"current_alerts"` // []
}
// NewTwc3FromConfig creates a new charger
func NewTwc3FromConfig(other map[string]any) (api.Charger, error) {
cc := struct {
URI string
Cache time.Duration
}{
Cache: time.Second,
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
c := &Twc3{}
client := request.NewHelper(util.NewLogger("twc3"))
uri := fmt.Sprintf("%s/api/1/vitals", util.DefaultScheme(strings.TrimSuffix(cc.URI, "/"), "http"))
c.vitalsG = util.Cached(func() (Vitals, error) {
var res Vitals
err := client.GetJSON(uri, &res)
return res, err
}, cc.Cache)
return c, nil
}
// Status implements the api.Charger interface
func (v *Twc3) Status() (api.ChargeStatus, error) {
status := api.StatusA // disconnected
res, err := v.vitalsG()
switch {
case res.ContactorClosed:
status = api.StatusC
case res.VehicleConnected:
status = api.StatusB
}
return status, err
}
// Enabled implements the api.Charger interface
func (c *Twc3) Enabled() (bool, error) {
return verifyEnabled(c, c.enabled)
}
// Enable implements the api.Charger interface
func (c *Twc3) Enable(enable bool) error {
if c.lp == nil {
return ErrLoadpointNotInitialized
}
// ignore disabling when vehicle is already disconnected
// https://github.com/evcc-io/evcc/issues/10213
status, err := c.Status()
if err != nil {
return err
}
if status == api.StatusA && !enable {
c.enabled = false
return nil
}
v, ok := api.Cap[api.ChargeController](c.lp.GetVehicle())
if !ok {
return errors.New("vehicle not capable of start/stop")
}
err = v.ChargeEnable(enable)
if err == nil {
c.enabled = enable
}
return err
}
// MaxCurrent implements the api.Charger interface
func (c *Twc3) MaxCurrent(current int64) error {
if c.lp == nil {
return ErrLoadpointNotInitialized
}
v, ok := api.Cap[api.CurrentController](c.lp.GetVehicle())
if !ok {
if c.lp.GetMode() == api.ModeNow {
// vehicle does not support current control- ignore silently
// since TWC3 cannot limit current on its own
return nil
}
return errors.New("vehicle not capable of current control")
}
return v.MaxCurrent(current)
}
var _ api.CurrentGetter = (*Twc3)(nil)
// GetMaxCurrent implements the api.CurrentGetter interface
func (c *Twc3) GetMaxCurrent() (float64, error) {
v, ok := api.Cap[api.CurrentGetter](c.lp.GetVehicle())
if !ok {
return 0, api.ErrNotAvailable
}
return v.GetMaxCurrent()
}
var _ api.ChargeRater = (*Twc3)(nil)
// ChargedEnergy implements the api.ChargeRater interface
func (v *Twc3) ChargedEnergy() (float64, error) {
res, err := v.vitalsG()
return res.SessionEnergyWh / 1e3, err
}
var _ api.ConnectionTimer = (*Twc3)(nil)
// ConnectionDuration implements the api.ConnectionTimer interface
func (v *Twc3) ConnectionDuration() (time.Duration, error) {
res, err := v.vitalsG()
return time.Duration(res.SessionS) * time.Second, err
}
// removed: https://github.com/evcc-io/evcc/issues/13555
// var _ api.ChargeTimer = (*Twc3)(nil)
// Use workaround if voltageC_v is approximately half of grid_v
//
// "voltageA_v": 241.5,
// "voltageB_v": 241.5,
// "voltageC_v": 118.7,
//
// Default state is ~2V on all phases unless charging
func (v *Twc3) isSplitPhase(res Vitals) bool {
return math.Abs(res.VoltageCV-res.GridV/2) < 25
}
var _ api.PhaseCurrents = (*Twc3)(nil)
// Currents implements the api.PhaseCurrents interface
func (v *Twc3) Currents() (float64, float64, float64, error) {
res, err := v.vitalsG()
if v.isSplitPhase(res) {
return res.CurrentAA + res.CurrentBA, 0, 0, err
}
return res.CurrentAA, res.CurrentBA, res.CurrentCA, err
}
var _ api.Meter = (*Twc3)(nil)
// CurrentPower implements the api.Meter interface
func (v *Twc3) CurrentPower() (float64, error) {
res, err := v.vitalsG()
if res.ContactorClosed {
if v.isSplitPhase(res) {
return (res.CurrentAA * res.VoltageAV) + (res.CurrentBA * res.VoltageBV), err
}
return (res.CurrentAA * res.VoltageAV) + (res.CurrentBA * res.VoltageBV) + (res.CurrentCA * res.VoltageCV), err
}
return 0, err
}
var _ api.PhaseVoltages = (*Twc3)(nil)
// Voltages implements the api.PhaseVoltages interface
func (v *Twc3) Voltages() (float64, float64, float64, error) {
res, err := v.vitalsG()
if v.isSplitPhase(res) {
return (res.VoltageAV + res.VoltageBV) / 2, 0, 0, err
}
return res.VoltageAV, res.VoltageBV, res.VoltageCV, err
}
var _ loadpoint.Controller = (*Twc3)(nil)
// LoadpointControl implements loadpoint.Controller
func (v *Twc3) LoadpointControl(lp loadpoint.API) {
v.lp = lp
}