diff --git a/templates/definition/vehicle/fiat.yaml b/templates/definition/vehicle/fiat.yaml index 66e7b0c92..f29f02ece 100644 --- a/templates/definition/vehicle/fiat.yaml +++ b/templates/definition/vehicle/fiat.yaml @@ -9,13 +9,13 @@ params: - name: pin mask: true help: - en: Required for evcc to refresh the SoC while charging. - de: Benötigt zur Aktualisierung des Ladestands während der Ladung. + en: Required for evcc to refresh the SoC while charging. When connected to TWC3, used to start/stop charging. + de: Benötigt zur Aktualisierung des Ladestands während der Ladung. Bei Nutzung mit TWC3 kann der Ladevorgang mittels PIN gestartet und gestoppt werden. - preset: vehicle-features render: | type: fiat {{ include "vehicle-base" . }} {{- if .pin }} - pin: {{ .pin }} # mandatory to deep refresh Soc + pin: {{ .pin }} # mandatory to deep refresh Soc & start/stop charge {{- end }} {{ include "vehicle-features" . }} diff --git a/vehicle/fiat.go b/vehicle/fiat.go index c219069ce..dc51cf9e9 100644 --- a/vehicle/fiat.go +++ b/vehicle/fiat.go @@ -15,6 +15,7 @@ import ( type Fiat struct { *embed *fiat.Provider + *fiat.Controller } func init() { @@ -59,6 +60,7 @@ func NewFiatFromConfig(other map[string]interface{}) (api.Vehicle, error) { if err == nil { v.Provider = fiat.NewProvider(api, cc.VIN, cc.PIN, cc.Expiry, cc.Cache) + v.Controller = fiat.NewController(v.Provider, api, cc.VIN, cc.PIN) } return v, err diff --git a/vehicle/fiat/api.go b/vehicle/fiat/api.go index ef0e0dd41..76e78df7c 100644 --- a/vehicle/fiat/api.go +++ b/vehicle/fiat/api.go @@ -158,3 +158,35 @@ func (v *API) Action(vin, pin, action, cmd string) (ActionResponse, error) { return res, err } + +func (v *API) UpdateSchedule(vin, pin string, schedules []Schedule) (ActionResponse, error) { + var res ActionResponse + + token, err := v.pinAuth(pin) + if err != nil { + return res, err + } + + uri := fmt.Sprintf("%s/v2/accounts/%s/vehicles/%s/ev/schedule", ApiURI, v.identity.UID(), vin) + + data := struct { + Command string `json:"command"` + PinAuth string `json:"pinAuth"` + Schedules []Schedule `json:"schedules"` + }{ + Command: "CPPLUS", + PinAuth: token, + Schedules: schedules, + } + + req, err := v.request(http.MethodPost, uri, request.MarshalJSON(data)) + if err == nil { + err = v.DoJSON(req, &res) + } + + if err == nil && res.Message != "" { + err = fmt.Errorf("action schedules: %s", res.Message) + } + + return res, err +} diff --git a/vehicle/fiat/controller.go b/vehicle/fiat/controller.go new file mode 100644 index 000000000..71b47e803 --- /dev/null +++ b/vehicle/fiat/controller.go @@ -0,0 +1,102 @@ +package fiat + +import ( + "fmt" + "time" + + "github.com/evcc-io/evcc/api" +) + +type Controller struct { + pvd *Provider + api *API + vin string + pin string +} + +// NewController creates a vehicle current and charge controller +func NewController(provider *Provider, api *API, vin string, pin string) *Controller { + impl := &Controller{ + pvd: provider, + api: api, + vin: vin, + pin: pin, + } + return impl +} + +var _ api.CurrentController = (*Controller)(nil) + +// MaxCurrent implements the api.CurrentController interface +func (c *Controller) MaxCurrent(current int64) error { + // Even if we cannot control the current, this interface must be implemented otherwise the ChargeEnable is never called + return nil +} + +var _ api.ChargeController = (*Controller)(nil) + +// ChargeEnable implements the api.ChargeController interface +func (c *Controller) ChargeEnable(enable bool) error { + if c.pin == "" { + return api.ErrMissingCredentials + } + + // get current schedule status from provider (cached) + stat, err := c.pvd.statusG() + if err != nil { + return err + } + if stat.EvInfo == nil || stat.EvInfo.Schedules == nil || len(stat.EvInfo.Schedules) == 0 { + return api.ErrNotAvailable + } + + // configure first schedule and make sure it's active + c.configureChargeSchedule(&stat.EvInfo.Schedules[0]) + + if enable { + // start charging by updating active charge schedule to start now and end in 12h + stat.EvInfo.Schedules[0].StartTime = time.Now().Format("15:04") // only hour and minutes + stat.EvInfo.Schedules[0].EndTime = time.Now().Add(time.Hour * 12).Format("15:04") // only hour and minutes + } else { + // stop charging by updating active charge schedule end time to now + stat.EvInfo.Schedules[0].EndTime = time.Now().Format("15:04") // only hour and minutes + } + + // make sure the other charge schedules are disabled in case user changed them + c.disableConflictingChargeSchedule(&stat.EvInfo.Schedules[1]) + c.disableConflictingChargeSchedule(&stat.EvInfo.Schedules[2]) + + // post new schedule + res, err := c.api.UpdateSchedule(c.vin, c.pin, stat.EvInfo.Schedules) + if err == nil && res.ResponseStatus != "pending" { + err = fmt.Errorf("invalid response status: %s", res.ResponseStatus) + } + + return err +} + +func (c *Controller) configureChargeSchedule(schedule *Schedule) { + // all values are set to be sure no manual change can lead to inconsistencies + schedule.CabinPriority = false + schedule.ChargeToFull = false + schedule.EnableScheduleType = true + schedule.RepeatSchedule = true + schedule.ScheduleType = "CHARGE" + + // only enable for current day to avoid undesired charge start in the future + weekday := time.Now().Weekday() + schedule.ScheduledDays.Monday = (weekday == time.Monday) + schedule.ScheduledDays.Tuesday = (weekday == time.Tuesday) + schedule.ScheduledDays.Wednesday = (weekday == time.Wednesday) + schedule.ScheduledDays.Thursday = (weekday == time.Thursday) + schedule.ScheduledDays.Friday = (weekday == time.Friday) + schedule.ScheduledDays.Saturday = (weekday == time.Saturday) + schedule.ScheduledDays.Sunday = (weekday == time.Sunday) +} + +func (c *Controller) disableConflictingChargeSchedule(schedule *Schedule) { + // make sure the other charge schedules are disabled in case user changed them + if schedule.ScheduleType == "CHARGE" && schedule.EnableScheduleType { + schedule.EnableScheduleType = false + } +} diff --git a/vehicle/fiat/types.go b/vehicle/fiat/types.go index c175fba05..babb24675 100644 --- a/vehicle/fiat/types.go +++ b/vehicle/fiat/types.go @@ -28,6 +28,25 @@ type Vehicle struct { VIN string } +type Schedule struct { + CabinPriority bool `json:"cabinPriority"` + ChargeToFull bool `json:"chargeToFull"` + EnableScheduleType bool `json:"enableScheduleType"` + EndTime string `json:"endTime"` + RepeatSchedule bool `json:"repeatSchedule"` + ScheduleType string `json:"scheduleType"` + ScheduledDays struct { + Friday bool `json:"friday"` + Monday bool `json:"monday"` + Saturday bool `json:"saturday"` + Sunday bool `json:"sunday"` + Thursday bool `json:"thursday"` + Tuesday bool `json:"tuesday"` + Wednesday bool `json:"wednesday"` + } `json:"scheduledDays"` + StartTime string `json:"startTime"` +} + type StatusResponse struct { VehicleInfo struct { Odometer struct { @@ -52,6 +71,7 @@ type StatusResponse struct { TimeToFullyChargeL2 int // 540 TotalRange int // 17 } + Schedules []Schedule Timestamp TimeMillis } `json:",omitempty"` Timestamp TimeMillis