Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/zaptec-cloud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ftw": minor
---

Zaptec Go, Go 2 and Pro can be added as a cloud EV charger. The setup wizard and Settings → EV offer Zaptec next to Easee; the same email and password list chargers on the account and drive current, pause and resume through Zaptec Cloud. The integration is experimental until a live charger has been exercised.
16 changes: 8 additions & 8 deletions go/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,14 +321,14 @@ type V2XPolicy struct {
// handler on POST /api/config. Providers that don't need auth (e.g. local
// Modbus) leave Username + Password empty.
type EVCharger struct {
Provider string `yaml:"provider" json:"provider"` // "easee" | "ctek"
Provider string `yaml:"provider" json:"provider"` // "easee" | "zaptec" | "ctek"

// Connection — populate the block matching the provider's transport.
HTTP *EVChargerHTTP `yaml:"http,omitempty" json:"http,omitempty"`
Modbus *EVChargerModbus `yaml:"modbus,omitempty" json:"modbus,omitempty"`

// Optional auth — required by cloud HTTP providers like Easee,
// unused by local Modbus providers like CTEK.
// Optional auth — required by cloud HTTP providers like Easee and
// Zaptec, unused by local Modbus providers like CTEK.
Username string `yaml:"username,omitempty" json:"username,omitempty"`
Password string `yaml:"-" json:"password,omitempty"` // persisted in state.db, not YAML

Expand Down Expand Up @@ -521,15 +521,15 @@ func (e *EVCharger) Validate() error {
switch e.Provider {
case "":
return errors.New("ev_charger.provider: required")
case "easee":
// Username/Password are NOT enforced here. The runtime easee
case "easee", "zaptec":
// Username/Password are NOT enforced here. The runtime cloud
// driver logs + idles when creds are missing, and the API picker
// requires both before calling Easee Cloud. Letting a partial
// requires both before calling the vendor cloud. Letting a partial
// ev_charger block load is the original contract — the wizard
// writes provider intent first, then captures creds in a second
// API call.
if e.Modbus != nil {
return errors.New("ev_charger.modbus: not valid for provider easee (HTTP transport)")
return fmt.Errorf("ev_charger.modbus: not valid for provider %s (HTTP transport)", e.Provider)
}
case "ctek":
if e.Modbus == nil || e.Modbus.Host == "" {
Expand All @@ -548,7 +548,7 @@ func (e *EVCharger) Validate() error {
return errors.New("ev_charger: username/password not valid for provider ctek")
}
default:
return fmt.Errorf("ev_charger.provider %q: not supported (valid: easee, ctek)", e.Provider)
return fmt.Errorf("ev_charger.provider %q: not supported (valid: easee, zaptec, ctek)", e.Provider)
}
return nil
}
Expand Down
30 changes: 30 additions & 0 deletions go/internal/config/evcharger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,36 @@ func TestEVChargerValidateEasee(t *testing.T) {
}
}

func TestEVChargerValidateZaptec(t *testing.T) {
cases := []struct {
name string
e EVCharger
wantErr string
}{
{"happy", EVCharger{Provider: "zaptec", Username: "u@x"}, ""},
{"empty creds allowed (wizard placeholder)", EVCharger{Provider: "zaptec"}, ""},
{"modbus block rejected", EVCharger{Provider: "zaptec", Username: "u@x", Modbus: &EVChargerModbus{Host: "h"}}, "modbus"},
{"http block allowed", EVCharger{Provider: "zaptec", Username: "u@x", HTTP: &EVChargerHTTP{BaseURL: "https://staging"}}, ""},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := tc.e.Validate()
if tc.wantErr == "" {
if err != nil {
t.Errorf("unexpected error: %v", err)
}
return
}
if err == nil {
t.Fatalf("expected error containing %q, got nil", tc.wantErr)
}
if !strings.Contains(err.Error(), tc.wantErr) {
t.Errorf("error %q does not contain %q", err.Error(), tc.wantErr)
}
})
}
}

func TestEVChargerValidateCTek(t *testing.T) {
cases := []struct {
name string
Expand Down
Loading