From aad484dbb00c7ad178f1eef2b995d573059fabf3 Mon Sep 17 00:00:00 2001 From: Aleix Date: Wed, 25 Dec 2024 12:56:49 +0100 Subject: [PATCH] Add V2C Trydan charger (#17878) --- charger/trydan.go | 188 +++++++++++++++++++++++ templates/definition/charger/trydan.yaml | 11 ++ 2 files changed, 199 insertions(+) create mode 100644 charger/trydan.go create mode 100644 templates/definition/charger/trydan.yaml diff --git a/charger/trydan.go b/charger/trydan.go new file mode 100644 index 000000000..38f7205ac --- /dev/null +++ b/charger/trydan.go @@ -0,0 +1,188 @@ +package charger + +// https://v2charge.com/trydan/ + +import ( + "errors" + "fmt" + "strings" + "time" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/provider" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/request" + "github.com/evcc-io/evcc/util/sponsor" +) + +type RealTimeData struct { + ID string `json:"ID"` + ChargeState int `json:"ChargeState"` + ReadyState int `json:"ReadyState"` + ChargePower float64 `json:"ChargePower"` + ChargeEnergy float64 `json:"ChargeEnergy"` + SlaveError int `json:"SlaveError"` + ChargeTime int `json:"ChargeTime"` + HousePower float64 `json:"HousePower"` + FVPower float64 `json:"FVPower"` + BatteryPower float64 `json:"BatteryPower"` + Paused int `json:"Paused"` + Locked int `json:"Locked"` + Timer int `json:"Timer"` + Intensity int `json:"Intensity"` + Dynamic int `json:"Dynamic"` + MinIntensity int `json:"MinIntensity"` + MaxIntensity int `json:"MaxIntensity"` + PauseDynamic int `json:"PauseDynamic"` + FirmwareVersion string `json:"FirmwareVersion"` + DynamicPowerMode int `json:"DynamicPowerMode"` + ContractedPower int `json:"ContractedPower"` +} + +// Trydan charger implementation +type Trydan struct { + *request.Helper + uri string + statusG provider.Cacheable[RealTimeData] + current int + enabled bool +} + +func init() { + registry.Add("trydan", NewTrydanFromConfig) +} + +// NewTrydanFromConfig creates a Trydan charger from generic config +func NewTrydanFromConfig(other map[string]interface{}) (api.Charger, error) { + cc := struct { + URI string + Cache time.Duration + }{ + Cache: time.Second, + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + if cc.URI == "" { + return nil, errors.New("missing uri") + } + + return NewTrydan(cc.URI, cc.Cache) +} + +// NewTrydan creates Trydan charger +func NewTrydan(uri string, cache time.Duration) (api.Charger, error) { + if !sponsor.IsAuthorized() { + return nil, api.ErrSponsorRequired + } + + c := &Trydan{ + Helper: request.NewHelper(util.NewLogger("trydan")), + uri: util.DefaultScheme(strings.TrimSuffix(uri, "/"), "http"), + } + + c.statusG = provider.ResettableCached(func() (RealTimeData, error) { + var res RealTimeData + uri := fmt.Sprintf("%s/RealTimeData", c.uri) + err := c.GetJSON(uri, &res) + return res, err + }, cache) + + return c, nil +} + +// Status implements the api.Charger interface +func (t Trydan) Status() (api.ChargeStatus, error) { + data, err := t.statusG.Get() + if err != nil { + return api.StatusNone, err + } + switch state := data.ChargeState; state { + case 0: + return api.StatusA, nil + case 1: + return api.StatusB, nil + case 2: + return api.StatusC, nil + default: + return api.StatusNone, fmt.Errorf("unknown status: %d", state) + } +} + +// Enabled implements the api.Charger interface +func (c Trydan) Enabled() (bool, error) { + data, err := c.statusG.Get() + return data.Paused == 0 && data.Locked == 0, err +} + +func (c *Trydan) setValue(param string, value int) error { + uri := fmt.Sprintf("%s/write/%s=%d", c.uri, param, value) + res, err := c.GetBody(uri) + if str := string(res); err == nil && str != "OK" { + err = fmt.Errorf("command failed: %s", res) + } + return err +} + +// Enable implements the api.Charger interface +func (c Trydan) Enable(enable bool) error { + var pause int + if !enable { + pause = 1 + } + + if err := c.setValue("Paused", pause); err != nil { + return err + } + if err := c.setValue("Locked", pause); err != nil { + return err + } + c.enabled = enable + + return nil +} + +// MaxCurrent implements the api.Charger interface +func (c Trydan) MaxCurrent(current int64) error { + err := c.setValue("Intensity", int(current)) + if err == nil { + c.current = int(current) + } + return err +} + +var _ api.ChargeRater = (*Trydan)(nil) + +// ChargedEnergy implements the api.ChargeRater interface +func (c Trydan) ChargedEnergy() (float64, error) { + data, err := c.statusG.Get() + return data.ChargeEnergy, err +} + +var _ api.ChargeTimer = (*Trydan)(nil) + +// ChargeDuration implements the api.ChargeTimer interface +func (c Trydan) ChargeDuration() (time.Duration, error) { + data, err := c.statusG.Get() + return time.Duration(data.ChargeTime) * time.Second, err +} + +var _ api.Meter = (*Trydan)(nil) + +// CurrentPower implements the api.Meter interface +func (c Trydan) CurrentPower() (float64, error) { + data, err := c.statusG.Get() + return data.ChargePower, err +} + +var _ api.Diagnosis = (*Trydan)(nil) + +// Diagnose implements the api.Diagnosis interface +func (c *Trydan) Diagnose() { + data, err := c.statusG.Get() + if err != nil { + fmt.Printf("%#v", data) + } +} diff --git a/templates/definition/charger/trydan.yaml b/templates/definition/charger/trydan.yaml new file mode 100644 index 000000000..1a82272b1 --- /dev/null +++ b/templates/definition/charger/trydan.yaml @@ -0,0 +1,11 @@ +template: trydan +products: + - brand: V2C Trydan +requirements: + description: + en: V2C trydan +params: + - name: host +render: | + type: trydan + uri: http://{{ .host }}