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 }