diff --git a/charger/helper.go b/charger/helper.go index 2433ee177..5b6d562cd 100644 --- a/charger/helper.go +++ b/charger/helper.go @@ -9,6 +9,9 @@ import ( "github.com/samber/lo" ) +// TODO remove when used +var _ = ensureCharger + // ensureCharger extracts ID from list of IDs returned from `list` function func ensureCharger(id string, list func() ([]string, error)) (string, error) { id, err := ensureChargerEx(id, list, func(v string) (string, error) { diff --git a/charger/zaptec.go b/charger/zaptec.go index bd439890e..4e9c61d68 100644 --- a/charger/zaptec.go +++ b/charger/zaptec.go @@ -19,6 +19,7 @@ package charger import ( "context" + "encoding/json" "errors" "fmt" "net/http" @@ -31,7 +32,6 @@ import ( "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" "github.com/evcc-io/evcc/util/sponsor" - "github.com/samber/lo" "golang.org/x/oauth2" ) @@ -41,11 +41,13 @@ import ( // Zaptec charger implementation type Zaptec struct { *request.Helper - log *util.Logger - statusG util.Cacheable[zaptec.StateResponse] - id string - enabled bool - priority bool + log *util.Logger + statusG util.Cacheable[zaptec.StateResponse] + instance zaptec.Charger + maxCurrent int + version int + enabled bool + priority bool } func init() { @@ -85,7 +87,6 @@ func NewZaptec(user, password, id string, priority bool, cache time.Duration) (a c := &Zaptec{ Helper: request.NewHelper(log), log: log, - id: id, priority: priority, } @@ -93,7 +94,7 @@ func NewZaptec(user, password, id string, priority bool, cache time.Duration) (a c.statusG = util.ResettableCached(func() (zaptec.StateResponse, error) { var res zaptec.StateResponse - uri := fmt.Sprintf("%s/api/chargers/%s/state", zaptec.ApiURL, c.id) + uri := fmt.Sprintf("%s/api/chargers/%s/state", zaptec.ApiURL, c.instance.Id) err := c.GetJSON(uri, &res) return res, err @@ -128,23 +129,54 @@ func NewZaptec(user, password, id string, priority bool, cache time.Duration) (a Base: c.Transport, } - c.id, err = ensureCharger(c.id, c.chargers) + c.instance, err = ensureChargerEx(id, c.chargers, func(charger zaptec.Charger) (string, error) { + return charger.Id, nil + }) + if err != nil { + return nil, err + } - return c, err + c.version, err = c.detectVersion() + if err != nil { + return nil, err + } + + c.maxCurrent, err = c.getMaxCurrent() + if err != nil { + return nil, err + } + return c, nil } -func (c *Zaptec) chargers() ([]string, error) { +func (c *Zaptec) detectVersion() (int, error) { + var capabilities zaptec.CapabilitiesResponse + + res, err := c.statusG.Get() + if err != nil { + return 0, err + } + + capResp := res.ObservationByID(zaptec.Capabilities) + if err := json.Unmarshal([]byte(capResp.ValueAsString), &capabilities); err != nil { + return 0, err + } + + if capabilities.ProductVariant == "Go2" { + return zaptec.ZaptecGo2, nil + } + + return zaptec.ZaptecGo1_Pro, nil +} + +func (c *Zaptec) chargers() ([]zaptec.Charger, error) { var res zaptec.ChargersResponse uri := fmt.Sprintf("%s/api/chargers", zaptec.ApiURL) - err := c.GetJSON(uri, &res) - if err == nil { - return lo.Map(res.Data, func(c zaptec.Charger, _ int) string { - return c.Id - }), nil + if err := c.GetJSON(uri, &res); err != nil { + return nil, err } - return nil, err + return res.Data, nil } // Status implements the api.Charger interface @@ -182,11 +214,11 @@ func (c *Zaptec) Enable(enable bool) error { cmd = zaptec.CmdResumeCharging } - uri := fmt.Sprintf("%s/api/chargers/%s/sendCommand/%d", zaptec.ApiURL, c.id, cmd) + uri := fmt.Sprintf("%s/api/chargers/%s/sendCommand/%d", zaptec.ApiURL, c.instance.Id, cmd) - req, err := request.New(http.MethodPost, uri, nil, request.JSONEncoding) + req, _ := request.New(http.MethodPost, uri, nil, request.JSONEncoding) + _, err := c.DoBody(req) if err == nil { - _, err = c.DoBody(req) c.enabled = enable c.statusG.Reset() } @@ -195,11 +227,11 @@ func (c *Zaptec) Enable(enable bool) error { } func (c *Zaptec) chargerUpdate(data zaptec.Update) error { - uri := fmt.Sprintf("%s/api/chargers/%s/update", zaptec.ApiURL, c.id) + uri := fmt.Sprintf("%s/api/chargers/%s/update", zaptec.ApiURL, c.instance.Id) - req, err := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding) + req, _ := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding) + _, err := c.DoBody(req) if err == nil { - _, err = c.DoBody(req) c.statusG.Reset() } @@ -209,9 +241,9 @@ func (c *Zaptec) chargerUpdate(data zaptec.Update) error { func (c *Zaptec) sessionPriority(session string, data zaptec.SessionPriority) error { uri := fmt.Sprintf("%s/api/session/%s/priority", zaptec.ApiURL, session) - req, err := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding) + req, _ := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding) + _, err := c.DoBody(req) if err == nil { - _, err = c.DoBody(req) c.statusG.Reset() } @@ -275,29 +307,51 @@ var _ api.PhaseSwitcher = (*Zaptec)(nil) // Phases1p3p implements the api.ChargePhases interface func (c *Zaptec) Phases1p3p(phases int) error { - res, err := c.statusG.Get() + err := c.switchPhases(phases) + if err != nil || !c.priority { + return err + } - if err == nil { + // priority configured + data := zaptec.SessionPriority{ + PrioritizedPhases: &phases, + } + + res, err := c.statusG.Get() + if err != nil { + return err + } + + if session := res.ObservationByID(zaptec.SessionIdentifier); session != nil { + return c.sessionPriority(session.ValueAsString, data) + } + + return errors.New("unknown session") +} + +func (c *Zaptec) switchPhases(phases int) error { + if c.version != zaptec.ZaptecGo2 { data := zaptec.Update{ MaxChargePhases: &phases, } - - err = c.chargerUpdate(data) + return c.chargerUpdate(data) } - if err == nil && c.priority { - data := zaptec.SessionPriority{ - PrioritizedPhases: &phases, - } - - if session := res.ObservationByID(zaptec.SessionIdentifier); session != nil { - err = c.sessionPriority(session.ValueAsString, data) - } else { - err = errors.New("unknown session") + var zero int + data := zaptec.UpdateInstallation{ + AvailableCurrentPhase1: &c.maxCurrent, + AvailableCurrentPhase2: &zero, + AvailableCurrentPhase3: &zero, + } + if phases == 3 { + data = zaptec.UpdateInstallation{ + AvailableCurrentPhase1: &c.maxCurrent, + AvailableCurrentPhase2: &c.maxCurrent, + AvailableCurrentPhase3: &c.maxCurrent, } } - return err + return c.installationUpdate(data) } var _ api.Identifier = (*Zaptec)(nil) @@ -316,6 +370,29 @@ func (c *Zaptec) Identify() (string, error) { return "", nil } +func (c *Zaptec) getMaxCurrent() (int, error) { + var res zaptec.Installation + + uri := fmt.Sprintf("%s/api/installation/%s", zaptec.ApiURL, c.instance.InstallationId) + if err := c.GetJSON(uri, &res); err != nil { + return 0, err + } + + return int(res.MaxCurrent), nil +} + +func (c *Zaptec) installationUpdate(data zaptec.UpdateInstallation) error { + uri := fmt.Sprintf("%s/api/installation/%s/update", zaptec.ApiURL, c.instance.InstallationId) + + req, _ := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding) + _, err := c.DoBody(req) + if err == nil { + c.statusG.Reset() + } + + return err +} + var _ api.Diagnosis = (*Zaptec)(nil) // Diagnosis implements the api.ChargePhases interface diff --git a/charger/zaptec/const.go b/charger/zaptec/const.go index e0226fc4e..c6ba83c4f 100644 --- a/charger/zaptec/const.go +++ b/charger/zaptec/const.go @@ -29,6 +29,7 @@ const ( const ( Unknown ObservationID = 0 OfflineMode ObservationID = 1 + Capabilities ObservationID = 100 AuthenticationRequired ObservationID = 120 PaymentActive ObservationID = 130 PaymentCurrency ObservationID = 131 @@ -140,3 +141,8 @@ const ( IsOnline ObservationID = -2 Pulse ObservationID = -1 ) + +const ( + ZaptecGo1_Pro = 0 + ZaptecGo2 = 1 +) diff --git a/charger/zaptec/types.go b/charger/zaptec/types.go index ac7422083..d6984a76b 100644 --- a/charger/zaptec/types.go +++ b/charger/zaptec/types.go @@ -1,6 +1,8 @@ package zaptec -import "strconv" +import ( + "strconv" +) type ChargersResponse struct { Pages int @@ -81,3 +83,23 @@ type Update struct { type SessionPriority struct { PrioritizedPhases *int `json:"prioritizedPhases,omitempty"` } + +type Installation struct { + Id string `json:"id"` + MaxCurrent float64 `json:"maxCurrent"` +} + +type UpdateInstallation struct { + AvailableCurrentPhase1 *int `json:"availableCurrentPhase1,omitempty"` + AvailableCurrentPhase2 *int `json:"availableCurrentPhase2,omitempty"` + AvailableCurrentPhase3 *int `json:"availableCurrentPhase3,omitempty"` +} + +type CapabilitiesResponse struct { + CommunicationModes []string `json:"communicationModes"` + DeviceType string `json:"deviceType"` + MeterCalibrated bool `json:"meterCalibrated"` + PhaseBalancing []string `json:"phaseBalancing"` + ProductVariant string `json:"productVariant"` + SchemaVersion string `json:"schemaVersion"` +} diff --git a/templates/definition/charger/zaptec.yaml b/templates/definition/charger/zaptec.yaml index 4e721c12f..84d062805 100644 --- a/templates/definition/charger/zaptec.yaml +++ b/templates/definition/charger/zaptec.yaml @@ -3,6 +3,9 @@ products: - brand: Zaptec description: generic: Go + - brand: Zaptec + description: + generic: Go 2 - brand: Zaptec description: generic: Pro