From 28ce2a3850d937cc1c33b3b9da3a690092833921 Mon Sep 17 00:00:00 2001 From: FraBoCH <19388397+FraBoCH@users.noreply.github.com> Date: Sat, 7 Feb 2026 15:32:43 +0100 Subject: [PATCH 01/17] Update controller.go Fix edge case of charge not stopping if charge session runs since the night before --- vehicle/fiat/controller.go | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/vehicle/fiat/controller.go b/vehicle/fiat/controller.go index df24cb59825..b2c6d5afb6b 100644 --- a/vehicle/fiat/controller.go +++ b/vehicle/fiat/controller.go @@ -54,16 +54,30 @@ func (c *Controller) ChargeEnable(enable bool) error { } // 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 - } + c.configureChargeSchedule(&stat.EvInfo.Schedules[0]) + + timeFormat := "15:04" + + if enable { + // start charging by updating active charge schedule to start now and end in 12h + stat.EvInfo.Schedules[0].StartTime = time.Now().Format(timeFormat) + stat.EvInfo.Schedules[0].EndTime = time.Now().Add(12 * time.Hour).Format(timeFormat) + } else { + // stop charging by updating active charge schedule end time to now + nowStr := time.Now().Format(timeFormat) + stat.EvInfo.Schedules[0].EndTime = nowStr + + // Parse times for comparison + start, err1 := time.Parse(timeFormat, stat.EvInfo.Schedules[0].StartTime) + end, err2 := time.Parse(timeFormat, nowStr) + + if err1 == nil && err2 == nil { + // If StartTime is after EndTime, fix it + if start.After(end) { + stat.EvInfo.Schedules[0].StartTime = "00:01" + } + } + } // make sure the other charge schedules are disabled in case user changed them c.disableConflictingChargeSchedule(&stat.EvInfo.Schedules[1]) From c25285210567547d60bfd3f5b0d893176913804c Mon Sep 17 00:00:00 2001 From: FraBoCH <19388397+FraBoCH@users.noreply.github.com> Date: Sat, 7 Feb 2026 20:19:22 +0100 Subject: [PATCH 02/17] Make code more explicit, avoid useless variable + fix ident --- vehicle/fiat/controller.go | 48 +++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/vehicle/fiat/controller.go b/vehicle/fiat/controller.go index b2c6d5afb6b..c8dd7f7a2b2 100644 --- a/vehicle/fiat/controller.go +++ b/vehicle/fiat/controller.go @@ -54,30 +54,30 @@ func (c *Controller) ChargeEnable(enable bool) error { } // configure first schedule and make sure it's active - c.configureChargeSchedule(&stat.EvInfo.Schedules[0]) - - timeFormat := "15:04" - - if enable { - // start charging by updating active charge schedule to start now and end in 12h - stat.EvInfo.Schedules[0].StartTime = time.Now().Format(timeFormat) - stat.EvInfo.Schedules[0].EndTime = time.Now().Add(12 * time.Hour).Format(timeFormat) - } else { - // stop charging by updating active charge schedule end time to now - nowStr := time.Now().Format(timeFormat) - stat.EvInfo.Schedules[0].EndTime = nowStr - - // Parse times for comparison - start, err1 := time.Parse(timeFormat, stat.EvInfo.Schedules[0].StartTime) - end, err2 := time.Parse(timeFormat, nowStr) - - if err1 == nil && err2 == nil { - // If StartTime is after EndTime, fix it - if start.After(end) { - stat.EvInfo.Schedules[0].StartTime = "00:01" - } - } - } + c.configureChargeSchedule(&stat.EvInfo.Schedules[0]) + + const ( + timeFormat = "15:04" + fallbackStartTime = "00:01" // Fallback time for schedules crossing midnight + ) + + currentTime := time.Now() // Call once and reuse + + if enable { + // Start charging: update active schedule with current time and end time (12h later) + stat.EvInfo.Schedules[0].StartTime = currentTime.Format(timeFormat) + stat.EvInfo.Schedules[0].EndTime = currentTime.Add(12 * time.Hour).Format(timeFormat) + } else { + // Stop charging: update end time to current time + stat.EvInfo.Schedules[0].EndTime = currentTime.Format(timeFormat) + + // Parse times for comparison and handle edge case: StartTime > EndTime + start, err1 := time.Parse(timeFormat, stat.EvInfo.Schedules[0].StartTime) + + if err1 == nil && start.After(currentTime) { + stat.EvInfo.Schedules[0].StartTime = fallbackStartTime + } + } // make sure the other charge schedules are disabled in case user changed them c.disableConflictingChargeSchedule(&stat.EvInfo.Schedules[1]) From 92c9348b3eb5f4527671f14ab8350a7a36c9011a Mon Sep 17 00:00:00 2001 From: FraBoCH <19388397+FraBoCH@users.noreply.github.com> Date: Sat, 7 Feb 2026 20:24:44 +0100 Subject: [PATCH 03/17] Added comment on time format --- vehicle/fiat/controller.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vehicle/fiat/controller.go b/vehicle/fiat/controller.go index c8dd7f7a2b2..883189f01cd 100644 --- a/vehicle/fiat/controller.go +++ b/vehicle/fiat/controller.go @@ -57,8 +57,8 @@ func (c *Controller) ChargeEnable(enable bool) error { c.configureChargeSchedule(&stat.EvInfo.Schedules[0]) const ( - timeFormat = "15:04" - fallbackStartTime = "00:01" // Fallback time for schedules crossing midnight + timeFormat = "15:04" // Hours & minutes only + fallbackStartTime = "00:01" // Fallback time for schedules crossing midnight ) currentTime := time.Now() // Call once and reuse From e549c74a413a1d5de7b6b51a2c9b1657fa4c072b Mon Sep 17 00:00:00 2001 From: FraBoCH <19388397+FraBoCH@users.noreply.github.com> Date: Sat, 7 Feb 2026 20:44:23 +0100 Subject: [PATCH 04/17] Fix lint issues --- vehicle/fiat/controller.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vehicle/fiat/controller.go b/vehicle/fiat/controller.go index 883189f01cd..ce9b4d29d72 100644 --- a/vehicle/fiat/controller.go +++ b/vehicle/fiat/controller.go @@ -57,10 +57,10 @@ func (c *Controller) ChargeEnable(enable bool) error { c.configureChargeSchedule(&stat.EvInfo.Schedules[0]) const ( - timeFormat = "15:04" // Hours & minutes only + timeFormat = "15:04" // Hours & minutes only fallbackStartTime = "00:01" // Fallback time for schedules crossing midnight ) - + currentTime := time.Now() // Call once and reuse if enable { @@ -74,8 +74,8 @@ func (c *Controller) ChargeEnable(enable bool) error { // Parse times for comparison and handle edge case: StartTime > EndTime start, err1 := time.Parse(timeFormat, stat.EvInfo.Schedules[0].StartTime) - if err1 == nil && start.After(currentTime) { - stat.EvInfo.Schedules[0].StartTime = fallbackStartTime + if err1 == nil && start.After(currentTime) { + stat.EvInfo.Schedules[0].StartTime = fallbackStartTime } } From c422e1a69b31c9cabd14decf00f094c417998265 Mon Sep 17 00:00:00 2001 From: FraBoCH <19388397+FraBoCH@users.noreply.github.com> Date: Sat, 7 Feb 2026 20:48:43 +0100 Subject: [PATCH 05/17] Fixed lint error --- vehicle/fiat/controller.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vehicle/fiat/controller.go b/vehicle/fiat/controller.go index ce9b4d29d72..20937cdd0dd 100644 --- a/vehicle/fiat/controller.go +++ b/vehicle/fiat/controller.go @@ -70,10 +70,10 @@ func (c *Controller) ChargeEnable(enable bool) error { } else { // Stop charging: update end time to current time stat.EvInfo.Schedules[0].EndTime = currentTime.Format(timeFormat) - + // Parse times for comparison and handle edge case: StartTime > EndTime start, err1 := time.Parse(timeFormat, stat.EvInfo.Schedules[0].StartTime) - + if err1 == nil && start.After(currentTime) { stat.EvInfo.Schedules[0].StartTime = fallbackStartTime } From e4382c69eefb9ab6ac512b896a6790b6734bac92 Mon Sep 17 00:00:00 2001 From: FraBoCH <19388397+FraBoCH@users.noreply.github.com> Date: Sat, 7 Feb 2026 20:55:22 +0100 Subject: [PATCH 06/17] Clarify condition & edge case --- vehicle/fiat/controller.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/vehicle/fiat/controller.go b/vehicle/fiat/controller.go index 20937cdd0dd..a5a8288e6a4 100644 --- a/vehicle/fiat/controller.go +++ b/vehicle/fiat/controller.go @@ -73,9 +73,8 @@ func (c *Controller) ChargeEnable(enable bool) error { // Parse times for comparison and handle edge case: StartTime > EndTime start, err1 := time.Parse(timeFormat, stat.EvInfo.Schedules[0].StartTime) - - if err1 == nil && start.After(currentTime) { - stat.EvInfo.Schedules[0].StartTime = fallbackStartTime + if err1 == nil && start.After(currentTime) { + stat.EvInfo.Schedules[0].StartTime = fallbackStartTime } } From 99b8f2a42b7a05bec81d3a03715529b2a56caa22 Mon Sep 17 00:00:00 2001 From: FraBoCH <19388397+FraBoCH@users.noreply.github.com> Date: Sun, 8 Feb 2026 20:26:25 +0100 Subject: [PATCH 07/17] Fixing points raised by @andig & adding rounding on end time As fiat system only supports schedules at 5 minutes interval, rounding to the next five minutes improves probability of the command be successful the first time --- vehicle/fiat/controller.go | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/vehicle/fiat/controller.go b/vehicle/fiat/controller.go index a5a8288e6a4..dbcd242a539 100644 --- a/vehicle/fiat/controller.go +++ b/vehicle/fiat/controller.go @@ -61,19 +61,22 @@ func (c *Controller) ChargeEnable(enable bool) error { fallbackStartTime = "00:01" // Fallback time for schedules crossing midnight ) - currentTime := time.Now() // Call once and reuse + now := time.Now() // Call once and reuse if enable { // Start charging: update active schedule with current time and end time (12h later) - stat.EvInfo.Schedules[0].StartTime = currentTime.Format(timeFormat) - stat.EvInfo.Schedules[0].EndTime = currentTime.Add(12 * time.Hour).Format(timeFormat) + stat.EvInfo.Schedules[0].StartTime = now.Format(timeFormat) + stat.EvInfo.Schedules[0].EndTime = now.Add(12 * time.Hour).Format(timeFormat) } else { - // Stop charging: update end time to current time - stat.EvInfo.Schedules[0].EndTime = currentTime.Format(timeFormat) + // Stop charging: update end time, rounded to next 5 mninutes + rounded := now.Truncate(5 * time.Minute) + if rounded.Before(now) { + rounded = rounded.Add(5 * time.Minute) + } + stat.EvInfo.Schedules[0].EndTime = rounded.Format(timeFormat) // Parse times for comparison and handle edge case: StartTime > EndTime - start, err1 := time.Parse(timeFormat, stat.EvInfo.Schedules[0].StartTime) - if err1 == nil && start.After(currentTime) { + if start, err := time.Parse(timeFormat, stat.EvInfo.Schedules[0].StartTime); err == nil && start.After(rounded) { stat.EvInfo.Schedules[0].StartTime = fallbackStartTime } } From 5e2e7f8ff171d43f8ce62bf3ccd7146c1cefdada Mon Sep 17 00:00:00 2001 From: FraBoCH <19388397+FraBoCH@users.noreply.github.com> Date: Sun, 8 Feb 2026 20:46:45 +0100 Subject: [PATCH 08/17] Fixed time comparison as per AI review suggestion Great catch! --- vehicle/fiat/controller.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vehicle/fiat/controller.go b/vehicle/fiat/controller.go index dbcd242a539..b62dcc00b0f 100644 --- a/vehicle/fiat/controller.go +++ b/vehicle/fiat/controller.go @@ -68,15 +68,15 @@ func (c *Controller) ChargeEnable(enable bool) error { stat.EvInfo.Schedules[0].StartTime = now.Format(timeFormat) stat.EvInfo.Schedules[0].EndTime = now.Add(12 * time.Hour).Format(timeFormat) } else { - // Stop charging: update end time, rounded to next 5 mninutes + // Stop charging: update end time, rounded to next 5 minutes to increase probability to be accepted by the car the first time rounded := now.Truncate(5 * time.Minute) if rounded.Before(now) { rounded = rounded.Add(5 * time.Minute) } stat.EvInfo.Schedules[0].EndTime = rounded.Format(timeFormat) - // Parse times for comparison and handle edge case: StartTime > EndTime - if start, err := time.Parse(timeFormat, stat.EvInfo.Schedules[0].StartTime); err == nil && start.After(rounded) { + // Parse times for comparison and handle edge case: StartTime > EndTime (hour & minutes only) + if start, err := time.Parse(timeFormat, stat.EvInfo.Schedules[0].StartTime); err == nil && start.Hour()*60+start.Minute() > rounded.Hour()*60+rounded.Minute()) { stat.EvInfo.Schedules[0].StartTime = fallbackStartTime } } From 747e9ee04ed15d4f0ba219e896d9a46aef3aae11 Mon Sep 17 00:00:00 2001 From: FraBoCH <19388397+FraBoCH@users.noreply.github.com> Date: Sun, 8 Feb 2026 20:51:21 +0100 Subject: [PATCH 09/17] Update controller.go --- vehicle/fiat/controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vehicle/fiat/controller.go b/vehicle/fiat/controller.go index b62dcc00b0f..89337b340ba 100644 --- a/vehicle/fiat/controller.go +++ b/vehicle/fiat/controller.go @@ -76,7 +76,7 @@ func (c *Controller) ChargeEnable(enable bool) error { stat.EvInfo.Schedules[0].EndTime = rounded.Format(timeFormat) // Parse times for comparison and handle edge case: StartTime > EndTime (hour & minutes only) - if start, err := time.Parse(timeFormat, stat.EvInfo.Schedules[0].StartTime); err == nil && start.Hour()*60+start.Minute() > rounded.Hour()*60+rounded.Minute()) { + if start, err := time.Parse(timeFormat, stat.EvInfo.Schedules[0].StartTime); err == nil && start.Hour()*60+start.Minute() > rounded.Hour()*60+rounded.Minute() { stat.EvInfo.Schedules[0].StartTime = fallbackStartTime } } From 3547e0d7d879311d90d4b15974c135dc142cb9fc Mon Sep 17 00:00:00 2001 From: FraBoCH <19388397+FraBoCH@users.noreply.github.com> Date: Wed, 11 Feb 2026 21:08:28 +0100 Subject: [PATCH 10/17] Another way to fix time comparison Instead playing with Time objects and edge case, simply parse defined schedule as string and make sure it's consistent --- vehicle/fiat/controller.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/vehicle/fiat/controller.go b/vehicle/fiat/controller.go index 89337b340ba..64d7fb36bb9 100644 --- a/vehicle/fiat/controller.go +++ b/vehicle/fiat/controller.go @@ -69,14 +69,16 @@ func (c *Controller) ChargeEnable(enable bool) error { stat.EvInfo.Schedules[0].EndTime = now.Add(12 * time.Hour).Format(timeFormat) } else { // Stop charging: update end time, rounded to next 5 minutes to increase probability to be accepted by the car the first time - rounded := now.Truncate(5 * time.Minute) - if rounded.Before(now) { - rounded = rounded.Add(5 * time.Minute) + roundedEndTime := now.Truncate(5 * time.Minute) + if roundedEndTime.Before(now) { + roundedEndTime = roundedEndTime.Add(5 * time.Minute) } - stat.EvInfo.Schedules[0].EndTime = rounded.Format(timeFormat) + stat.EvInfo.Schedules[0].EndTime = roundedEndTime.Format(timeFormat) - // Parse times for comparison and handle edge case: StartTime > EndTime (hour & minutes only) - if start, err := time.Parse(timeFormat, stat.EvInfo.Schedules[0].StartTime); err == nil && start.Hour()*60+start.Minute() > rounded.Hour()*60+rounded.Minute() { + // Make sure start time is always before end time (parse both from string to ensure proper comparison) + start, err1 := time.Parse(timeFormat, stat.EvInfo.Schedules[0].StartTime) + end, err2 := time.Parse(timeFormat, stat.EvInfo.Schedules[0].EndTime) + if err1 == nil && err2 == nill && start.After(end) { stat.EvInfo.Schedules[0].StartTime = fallbackStartTime } } From 93af4653f2cd1aa9c609bbea08bba082ba2eb784 Mon Sep 17 00:00:00 2001 From: FraBoCH <19388397+FraBoCH@users.noreply.github.com> Date: Wed, 11 Feb 2026 21:12:05 +0100 Subject: [PATCH 11/17] typo --- vehicle/fiat/controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vehicle/fiat/controller.go b/vehicle/fiat/controller.go index 64d7fb36bb9..24471c91cbf 100644 --- a/vehicle/fiat/controller.go +++ b/vehicle/fiat/controller.go @@ -78,7 +78,7 @@ func (c *Controller) ChargeEnable(enable bool) error { // Make sure start time is always before end time (parse both from string to ensure proper comparison) start, err1 := time.Parse(timeFormat, stat.EvInfo.Schedules[0].StartTime) end, err2 := time.Parse(timeFormat, stat.EvInfo.Schedules[0].EndTime) - if err1 == nil && err2 == nill && start.After(end) { + if err1 == nil && err2 == nil && start.After(end) { stat.EvInfo.Schedules[0].StartTime = fallbackStartTime } } From 3525dbdd0a51a151cc2cf7c7669ed07bd4b2ad88 Mon Sep 17 00:00:00 2001 From: FraBoch <19388397+FraBoCH@users.noreply.github.com> Date: Sat, 14 Feb 2026 20:52:41 +0100 Subject: [PATCH 12/17] Refactor charge schedule configuration to improve time handling and ensure proper API acceptance --- vehicle/fiat/controller.go | 77 ++++++++++++++++++++++++++------------ 1 file changed, 53 insertions(+), 24 deletions(-) diff --git a/vehicle/fiat/controller.go b/vehicle/fiat/controller.go index 24471c91cbf..bb6a36851c3 100644 --- a/vehicle/fiat/controller.go +++ b/vehicle/fiat/controller.go @@ -54,33 +54,15 @@ func (c *Controller) ChargeEnable(enable bool) error { } // configure first schedule and make sure it's active - c.configureChargeSchedule(&stat.EvInfo.Schedules[0]) - - const ( - timeFormat = "15:04" // Hours & minutes only - fallbackStartTime = "00:01" // Fallback time for schedules crossing midnight - ) - now := time.Now() // Call once and reuse if enable { - // Start charging: update active schedule with current time and end time (12h later) - stat.EvInfo.Schedules[0].StartTime = now.Format(timeFormat) - stat.EvInfo.Schedules[0].EndTime = now.Add(12 * time.Hour).Format(timeFormat) + // Start charging: configure charge from now to 12h later + in12hours := now.Add(12 * time.Hour) + c.configureChargeSchedule(&stat.EvInfo.Schedules[0], now, in12hours) } else { - // Stop charging: update end time, rounded to next 5 minutes to increase probability to be accepted by the car the first time - roundedEndTime := now.Truncate(5 * time.Minute) - if roundedEndTime.Before(now) { - roundedEndTime = roundedEndTime.Add(5 * time.Minute) - } - stat.EvInfo.Schedules[0].EndTime = roundedEndTime.Format(timeFormat) - - // Make sure start time is always before end time (parse both from string to ensure proper comparison) - start, err1 := time.Parse(timeFormat, stat.EvInfo.Schedules[0].StartTime) - end, err2 := time.Parse(timeFormat, stat.EvInfo.Schedules[0].EndTime) - if err1 == nil && err2 == nil && start.After(end) { - stat.EvInfo.Schedules[0].StartTime = fallbackStartTime - } + // Stop charging: update end time (use empty time to keep start time as it was for history in Fiat app) + c.configureChargeSchedule(&stat.EvInfo.Schedules[0], time.Time{}, now) } // make sure the other charge schedules are disabled in case user changed them @@ -96,7 +78,22 @@ func (c *Controller) ChargeEnable(enable bool) error { return err } -func (c *Controller) configureChargeSchedule(schedule *Schedule) { +func roundUpTo(d time.Duration, t time.Time) time.Time { + // Round up to next d boundary + rt := t.Truncate(d) + if !rt.After(t) { + rt = rt.Add(d) + } + return rt +} + +func (c *Controller) configureChargeSchedule(schedule *Schedule, start time.Time, end time.Time) { + const ( + timeFormat = "15:04" // Hours & minutes only + fallbackStartTime = "00:01" // Fallback time for schedules crossing midnight + minTimeInterval = 5 * time.Minute // Minimum time interval accepted by Fiat API in schedules; used for rounding up start and end time to avoid API rejections + ) + // all values are set to be sure no manual change can lead to inconsistencies schedule.CabinPriority = false schedule.ChargeToFull = false @@ -113,6 +110,38 @@ func (c *Controller) configureChargeSchedule(schedule *Schedule) { schedule.ScheduledDays.Friday = (weekday == time.Friday) schedule.ScheduledDays.Saturday = (weekday == time.Saturday) schedule.ScheduledDays.Sunday = (weekday == time.Sunday) + + // Update start only if provided (non-zero) + if !start.IsZero() { + // TODO: test with out rounding & round DOWN. Round up only as last resort. + //rounded := roundUpTo(minTimeInterval, *start) + schedule.StartTime = start.Format(timeFormat) + c.log.DEBUG.Printf("set charge schedule start: %s", schedule.StartTime) + } + + // Update end only if provided (non-zero); round up to next 5 minutes boundary to increase chance of API accepting the schedule the first time + if !end.IsZero() { + rounded := roundUpTo(minTimeInterval, end) + schedule.EndTime = rounded.Format(timeFormat) + c.log.DEBUG.Printf("set charge schedule end: %s (rounded from %s)", schedule.EndTime, end.Format(timeFormat)) + } + + // If one of the time changed, make sure start time is always before end time (parse both from string to ensure proper comparison) + if !start.IsZero() || !end.IsZero() { + chkStart, err1 := time.Parse(timeFormat, schedule.StartTime) + chkEnd, err2 := time.Parse(timeFormat, schedule.EndTime) + if err1 == nil && err2 == nil && chkStart.After(chkEnd) { + // If start time is after end time, set start time to fallback value (00:01) to avoid API rejections for schedules crossing midnight + c.log.DEBUG.Printf("start time %s is after end time %s, setting start time to fallback value %s", schedule.StartTime, schedule.EndTime, fallbackStartTime) + schedule.StartTime = fallbackStartTime + } else if err1 != nil || err2 != nil { + c.log.WARN.Printf("failed to parse schedule times: start=%v, end=%v", err1, err2) + if err1 != nil { + // If start time cannot be parsed, also set to fallback value + schedule.StartTime = fallbackStartTime + } + } + } } func (c *Controller) disableConflictingChargeSchedule(schedule *Schedule) { From 5edfd195e41d888b0b8f19d25a01646d63f17a28 Mon Sep 17 00:00:00 2001 From: FraBoch <19388397+FraBoCH@users.noreply.github.com> Date: Sun, 15 Feb 2026 21:06:42 +0100 Subject: [PATCH 13/17] Enhance charge schedule management to avoid unnecessary API calls and ensure proper time rounding to avoid API errors --- vehicle/fiat/controller.go | 142 +++++++++++++++++++++++++------------ 1 file changed, 95 insertions(+), 47 deletions(-) diff --git a/vehicle/fiat/controller.go b/vehicle/fiat/controller.go index bb6a36851c3..bce59f30df9 100644 --- a/vehicle/fiat/controller.go +++ b/vehicle/fiat/controller.go @@ -53,102 +53,150 @@ func (c *Controller) ChargeEnable(enable bool) error { return api.ErrNotAvailable } - // configure first schedule and make sure it's active - now := time.Now() // Call once and reuse + hasChanged := false // Will track if we made any change to the schedule to avoid unnecessary updates through API call + now := time.Now() // Call once and reuse if enable { // Start charging: configure charge from now to 12h later in12hours := now.Add(12 * time.Hour) - c.configureChargeSchedule(&stat.EvInfo.Schedules[0], now, in12hours) + hasChanged = c.configureChargeSchedule(&stat.EvInfo.Schedules[0], now, in12hours) } else { // Stop charging: update end time (use empty time to keep start time as it was for history in Fiat app) - c.configureChargeSchedule(&stat.EvInfo.Schedules[0], time.Time{}, now) + hasChanged = c.configureChargeSchedule(&stat.EvInfo.Schedules[0], time.Time{}, now) } // 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) + hasChanged = hasChanged || c.disableConflictingChargeSchedule(&stat.EvInfo.Schedules[1]) + hasChanged = hasChanged || c.disableConflictingChargeSchedule(&stat.EvInfo.Schedules[2]) + + // post new schedule, but only if something changed to avoid unnecessary API calls + if hasChanged { + res, err := c.api.UpdateSchedule(c.vin, c.pin, stat.EvInfo.Schedules) + c.log.INFO.Printf("updated first charge schedule: enable=%v, start=%s, end=%s", enable, stat.EvInfo.Schedules[0].StartTime, stat.EvInfo.Schedules[0].EndTime) + if err == nil && res.ResponseStatus != "pending" { + err = fmt.Errorf("invalid response status: %s", res.ResponseStatus) + } } return err } -func roundUpTo(d time.Duration, t time.Time) time.Time { - // Round up to next d boundary - rt := t.Truncate(d) - if !rt.After(t) { - rt = rt.Add(d) +// computeNewApiScheduleTime computes the new schedule time to set in the API based on the current schedule time and the target time provided by the user, while ensuring it fits API requirements (rounding up to next 5 minutes boundary and avoiding changes if time difference is not significant to prevent API rejections for unchanged schedules) +func (c *Controller) computeNewApiScheduleTime(current string, target time.Time, timeFormat string) string { + + const ( + minTimeInterval = 5 * time.Minute // Minimum time interval accepted by Fiat API in schedules; used for rounding up start and end time to avoid API rejections + ) + + // By default, return current time unchanged to avoid changing if target time is not significantly different from current time + result := current + + // Parse previous schedule time to detect if this is a meaningful change + currentTime, err1 := time.Parse(timeFormat, current) + targetTime, err2 := time.Parse(timeFormat, target.Format(timeFormat)) // Format target time to same format as current time for proper comparison + timeDiff := targetTime.Sub(currentTime).Abs() + c.log.DEBUG.Printf("current schedule time: %s, target time: %s, time difference: %s", currentTime.Format(timeFormat), targetTime.Format(timeFormat), timeDiff) + + // Round up only if end time changed significantly (>1 min) or if parsing previous time failed + if err1 != nil || err2 != nil || timeDiff > time.Minute { + // round up to next 5 minutes boundary to avoid API rejections + roundedTarget := target.Truncate(minTimeInterval) + if roundedTarget.Before(target) { + roundedTarget = roundedTarget.Add(minTimeInterval) + } + result = roundedTarget.Format(timeFormat) + c.log.DEBUG.Printf("target time %s rounded to %s to fit API requirements", target.Format(timeFormat), result) } - return rt + + return result } -func (c *Controller) configureChargeSchedule(schedule *Schedule, start time.Time, end time.Time) { +// configureChargeSchedule configures the provided schedule with the provided start and end time, while ensuring it fits API requirements and avoiding unnecessary changes if times are not significantly different to prevent API rejections for unchanged schedules. It returns true if the schedule was changed and false otherwise. +func (c *Controller) configureChargeSchedule(schedule *Schedule, start time.Time, end time.Time) bool { const ( - timeFormat = "15:04" // Hours & minutes only - fallbackStartTime = "00:01" // Fallback time for schedules crossing midnight - minTimeInterval = 5 * time.Minute // Minimum time interval accepted by Fiat API in schedules; used for rounding up start and end time to avoid API rejections + timeFormat = "15:04" // Hours & minutes only + fallbackStartTime = "00:00" // Fallback time for schedules crossing midnight ) - // 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) + hasChanged := false // track if we made any change to the schedule to avoid unnecessary API calls + + // Make sure schedule is enabled and of type CHARGE + if schedule.ScheduleType != "CHARGE" || !schedule.EnableScheduleType { + schedule.ScheduleType = "CHARGE" + schedule.EnableScheduleType = true + schedule.CabinPriority = false + schedule.ChargeToFull = false + schedule.RepeatSchedule = true + hasChanged = true + c.log.DEBUG.Printf("schedule type changed to CHARGE and enabled") + } // Update start only if provided (non-zero) if !start.IsZero() { - // TODO: test with out rounding & round DOWN. Round up only as last resort. - //rounded := roundUpTo(minTimeInterval, *start) - schedule.StartTime = start.Format(timeFormat) - c.log.DEBUG.Printf("set charge schedule start: %s", schedule.StartTime) + newStartStr := c.computeNewApiScheduleTime(schedule.StartTime, start, timeFormat) + + // Update only if different from current + if newStartStr != schedule.StartTime { + schedule.StartTime = newStartStr + hasChanged = true + c.log.DEBUG.Printf("set charge schedule start: %s", schedule.StartTime) + + // 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) + } } - // Update end only if provided (non-zero); round up to next 5 minutes boundary to increase chance of API accepting the schedule the first time + // Update end only if provided (non-zero) if !end.IsZero() { - rounded := roundUpTo(minTimeInterval, end) - schedule.EndTime = rounded.Format(timeFormat) - c.log.DEBUG.Printf("set charge schedule end: %s (rounded from %s)", schedule.EndTime, end.Format(timeFormat)) + newEndStr := c.computeNewApiScheduleTime(schedule.EndTime, end, timeFormat) + + // Update only if different from current + if newEndStr != schedule.EndTime { + schedule.EndTime = newEndStr + hasChanged = true + c.log.DEBUG.Printf("set charge schedule end: %s", schedule.EndTime) + } } // If one of the time changed, make sure start time is always before end time (parse both from string to ensure proper comparison) - if !start.IsZero() || !end.IsZero() { + if (!start.IsZero() || !end.IsZero()) && hasChanged { chkStart, err1 := time.Parse(timeFormat, schedule.StartTime) chkEnd, err2 := time.Parse(timeFormat, schedule.EndTime) if err1 == nil && err2 == nil && chkStart.After(chkEnd) { // If start time is after end time, set start time to fallback value (00:01) to avoid API rejections for schedules crossing midnight c.log.DEBUG.Printf("start time %s is after end time %s, setting start time to fallback value %s", schedule.StartTime, schedule.EndTime, fallbackStartTime) schedule.StartTime = fallbackStartTime + hasChanged = true } else if err1 != nil || err2 != nil { c.log.WARN.Printf("failed to parse schedule times: start=%v, end=%v", err1, err2) if err1 != nil { // If start time cannot be parsed, also set to fallback value schedule.StartTime = fallbackStartTime + hasChanged = true + c.log.DEBUG.Printf("set charge schedule start to fallback value %s due to parse error", fallbackStartTime) } } } + + return hasChanged } -func (c *Controller) disableConflictingChargeSchedule(schedule *Schedule) { +// disableConflictingChargeSchedule makes sure the provided schedule is disabled if it's of type CHARGE to avoid conflicts between schedules and potential API rejections for conflicting schedules. It returns true if the schedule was changed and false otherwise. +func (c *Controller) disableConflictingChargeSchedule(schedule *Schedule) bool { // make sure the other charge schedules are disabled in case user changed them if schedule.ScheduleType == "CHARGE" && schedule.EnableScheduleType { schedule.EnableScheduleType = false + c.log.INFO.Printf("disabled charge schedule other than the first one to avoid conflicts") + return true // schedule was changed } + return false // schedule was not changed } var _ api.Resurrector = (*Controller)(nil) From 1509a8da02a4aee7d2bd3cbc70a6801dd75db4d2 Mon Sep 17 00:00:00 2001 From: FraBoch <19388397+FraBoCH@users.noreply.github.com> Date: Sat, 21 Feb 2026 12:38:52 +0100 Subject: [PATCH 14/17] Fixed issue with end time after midnight & increase allowed time difference before update schedule --- vehicle/fiat/controller.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/vehicle/fiat/controller.go b/vehicle/fiat/controller.go index bce59f30df9..0be58241c11 100644 --- a/vehicle/fiat/controller.go +++ b/vehicle/fiat/controller.go @@ -57,9 +57,8 @@ func (c *Controller) ChargeEnable(enable bool) error { now := time.Now() // Call once and reuse if enable { - // Start charging: configure charge from now to 12h later - in12hours := now.Add(12 * time.Hour) - hasChanged = c.configureChargeSchedule(&stat.EvInfo.Schedules[0], now, in12hours) + // Start charging: configure charge from now (end time will be handled in computeNewApiScheduleTime) + hasChanged = c.configureChargeSchedule(&stat.EvInfo.Schedules[0], now, time.Time{}) // only set start time to now and keep end time unchanged to avoid undesired charge stop in the future } else { // Stop charging: update end time (use empty time to keep start time as it was for history in Fiat app) hasChanged = c.configureChargeSchedule(&stat.EvInfo.Schedules[0], time.Time{}, now) @@ -97,8 +96,8 @@ func (c *Controller) computeNewApiScheduleTime(current string, target time.Time, timeDiff := targetTime.Sub(currentTime).Abs() c.log.DEBUG.Printf("current schedule time: %s, target time: %s, time difference: %s", currentTime.Format(timeFormat), targetTime.Format(timeFormat), timeDiff) - // Round up only if end time changed significantly (>1 min) or if parsing previous time failed - if err1 != nil || err2 != nil || timeDiff > time.Minute { + // Round up only if end time changed significantly or if parsing previous time failed + if err1 != nil || err2 != nil || timeDiff > (minTimeInterval/2) { // round up to next 5 minutes boundary to avoid API rejections roundedTarget := target.Truncate(minTimeInterval) if roundedTarget.Before(target) { @@ -115,6 +114,7 @@ func (c *Controller) computeNewApiScheduleTime(current string, target time.Time, func (c *Controller) configureChargeSchedule(schedule *Schedule, start time.Time, end time.Time) bool { const ( timeFormat = "15:04" // Hours & minutes only + defaultEndTime = "23:55" // Default end time to use when enabling charge to avoid API rejections for schedules without end time; set to end of the day to avoid undesired charge stop in the future fallbackStartTime = "00:00" // Fallback time for schedules crossing midnight ) @@ -138,8 +138,9 @@ func (c *Controller) configureChargeSchedule(schedule *Schedule, start time.Time // Update only if different from current if newStartStr != schedule.StartTime { schedule.StartTime = newStartStr + schedule.EndTime = defaultEndTime // Set default end time when enabling charge to avoid API rejections for schedules without end time hasChanged = true - c.log.DEBUG.Printf("set charge schedule start: %s", schedule.StartTime) + c.log.DEBUG.Printf("set charge schedule start: %s with default end time: %s", schedule.StartTime, schedule.EndTime) // only enable for current day to avoid undesired charge start in the future weekday := time.Now().Weekday() From 0b09e22222efb70e070e43f383d9cad696eceb4c Mon Sep 17 00:00:00 2001 From: FraBoch <19388397+FraBoCH@users.noreply.github.com> Date: Sat, 21 Feb 2026 13:20:57 +0100 Subject: [PATCH 15/17] Fix wakeup forcing charge --- vehicle/fiat/api.go | 5 +++++ vehicle/fiat/controller.go | 11 +++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/vehicle/fiat/api.go b/vehicle/fiat/api.go index 011f81127d2..7449dc351fe 100644 --- a/vehicle/fiat/api.go +++ b/vehicle/fiat/api.go @@ -159,10 +159,15 @@ func (v *API) Action(vin, pin, action, cmd string) (ActionResponse, error) { return res, err } +// Warning: calling ChargeNow will start charging immediately and schedules will not be able to stop the charging. func (v *API) ChargeNow(vin, pin string) (ActionResponse, error) { return v.Action(vin, pin, "ev/chargenow", "CNOW") } +func (v *API) DeepRefresh(vin, pin string) (ActionResponse, error) { + return v.Action(vin, pin, "ev", "DEEPREFRESH") +} + func (v *API) UpdateSchedule(vin, pin string, schedules []Schedule) (ActionResponse, error) { var res ActionResponse diff --git a/vehicle/fiat/controller.go b/vehicle/fiat/controller.go index 0be58241c11..256cf7985d1 100644 --- a/vehicle/fiat/controller.go +++ b/vehicle/fiat/controller.go @@ -97,7 +97,7 @@ func (c *Controller) computeNewApiScheduleTime(current string, target time.Time, c.log.DEBUG.Printf("current schedule time: %s, target time: %s, time difference: %s", currentTime.Format(timeFormat), targetTime.Format(timeFormat), timeDiff) // Round up only if end time changed significantly or if parsing previous time failed - if err1 != nil || err2 != nil || timeDiff > (minTimeInterval/2) { + if err1 != nil || err2 != nil || timeDiff > time.Minute { // round up to next 5 minutes boundary to avoid API rejections roundedTarget := target.Truncate(minTimeInterval) if roundedTarget.Before(target) { @@ -204,12 +204,15 @@ var _ api.Resurrector = (*Controller)(nil) func (c *Controller) WakeUp() error { if c.pin == "" { - c.log.DEBUG.Printf("pin required for vehicle wakeup") + c.log.DEBUG.Printf("Vehicle cannot be woken up: no PIN provided") return nil } - res, err := c.api.ChargeNow(c.vin, c.pin) - if err == nil && res.ResponseStatus != "pending" { + // Trigger deep refresh to wake up the vehicle, and this requires the same pin authentication as other actions + res, err := c.api.DeepRefresh(c.vin, c.pin) + if err != nil && res.ResponseStatus == "pending" { + c.log.DEBUG.Printf("vehicle wakeup triggered successfully with deep refresh action") + } else { err = fmt.Errorf("invalid response status: %s", res.ResponseStatus) } From 80c88d0177220c2c978b4214ac32391d208e79da Mon Sep 17 00:00:00 2001 From: FraBoch <19388397+FraBoCH@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:13:51 +0100 Subject: [PATCH 16/17] refactor: improve charge control logic and error handling in Fiat vehicle API --- vehicle/fiat/api.go | 6 +-- vehicle/fiat/controller.go | 108 ++++++++++++++++++------------------- 2 files changed, 53 insertions(+), 61 deletions(-) diff --git a/vehicle/fiat/api.go b/vehicle/fiat/api.go index 7449dc351fe..4fa14634f9e 100644 --- a/vehicle/fiat/api.go +++ b/vehicle/fiat/api.go @@ -164,10 +164,6 @@ func (v *API) ChargeNow(vin, pin string) (ActionResponse, error) { return v.Action(vin, pin, "ev/chargenow", "CNOW") } -func (v *API) DeepRefresh(vin, pin string) (ActionResponse, error) { - return v.Action(vin, pin, "ev", "DEEPREFRESH") -} - func (v *API) UpdateSchedule(vin, pin string, schedules []Schedule) (ActionResponse, error) { var res ActionResponse @@ -194,7 +190,7 @@ func (v *API) UpdateSchedule(vin, pin string, schedules []Schedule) (ActionRespo } if err == nil && res.Message != "" { - err = fmt.Errorf("action schedules: %s", res.Message) + err = fmt.Errorf("unable to set action schedules: %s", res.Message) } return res, err diff --git a/vehicle/fiat/controller.go b/vehicle/fiat/controller.go index 256cf7985d1..611cc063a6c 100644 --- a/vehicle/fiat/controller.go +++ b/vehicle/fiat/controller.go @@ -9,21 +9,23 @@ import ( ) type Controller struct { - pvd *Provider - api *API - log *util.Logger - vin string - pin string + pvd *Provider + api *API + log *util.Logger + vin string + pin string + isChargedControlled bool } // NewController creates a vehicle current and charge controller func NewController(provider *Provider, api *API, log *util.Logger, vin string, pin string) *Controller { impl := &Controller{ - pvd: provider, - api: api, - log: log, - vin: vin, - pin: pin, + pvd: provider, + api: api, + log: log, + vin: vin, + pin: pin, + isChargedControlled: false, // false for now, will be set to true when ChargeEnable is actually called. } return impl } @@ -44,6 +46,8 @@ func (c *Controller) ChargeEnable(enable bool) error { return api.ErrMissingCredentials } + c.isChargedControlled = true // set to true when ChargeEnable is called for the first time, to indicate that we are actually controlling the charge, this will be used in WakeUp method to decide if we should call ChargeNow or not + // get current schedule status from provider (cached) stat, err := c.pvd.statusG() if err != nil { @@ -57,10 +61,10 @@ func (c *Controller) ChargeEnable(enable bool) error { now := time.Now() // Call once and reuse if enable { - // Start charging: configure charge from now (end time will be handled in computeNewApiScheduleTime) - hasChanged = c.configureChargeSchedule(&stat.EvInfo.Schedules[0], now, time.Time{}) // only set start time to now and keep end time unchanged to avoid undesired charge stop in the future + // Start charging from now until end of day (23:55) + hasChanged = c.configureChargeSchedule(&stat.EvInfo.Schedules[0], now, time.Time{}) } else { - // Stop charging: update end time (use empty time to keep start time as it was for history in Fiat app) + // Stop charging: set charge end time to now to stop charging immediately (use empty time to keep start time as it was for history in Fiat app) hasChanged = c.configureChargeSchedule(&stat.EvInfo.Schedules[0], time.Time{}, now) } @@ -71,51 +75,35 @@ func (c *Controller) ChargeEnable(enable bool) error { // post new schedule, but only if something changed to avoid unnecessary API calls if hasChanged { res, err := c.api.UpdateSchedule(c.vin, c.pin, stat.EvInfo.Schedules) - c.log.INFO.Printf("updated first charge schedule: enable=%v, start=%s, end=%s", enable, stat.EvInfo.Schedules[0].StartTime, stat.EvInfo.Schedules[0].EndTime) - if err == nil && res.ResponseStatus != "pending" { - err = fmt.Errorf("invalid response status: %s", res.ResponseStatus) + if err != nil { + return fmt.Errorf("failed to update schedule: %w", err) + } + if res.ResponseStatus != "pending" { + return fmt.Errorf("invalid response status: %s", res.ResponseStatus) } + c.log.INFO.Printf("updated first charge schedule: enable=%v, start=%s, end=%s", + enable, stat.EvInfo.Schedules[0].StartTime, stat.EvInfo.Schedules[0].EndTime) } - return err + return nil } -// computeNewApiScheduleTime computes the new schedule time to set in the API based on the current schedule time and the target time provided by the user, while ensuring it fits API requirements (rounding up to next 5 minutes boundary and avoiding changes if time difference is not significant to prevent API rejections for unchanged schedules) -func (c *Controller) computeNewApiScheduleTime(current string, target time.Time, timeFormat string) string { - - const ( - minTimeInterval = 5 * time.Minute // Minimum time interval accepted by Fiat API in schedules; used for rounding up start and end time to avoid API rejections - ) - - // By default, return current time unchanged to avoid changing if target time is not significantly different from current time - result := current - - // Parse previous schedule time to detect if this is a meaningful change - currentTime, err1 := time.Parse(timeFormat, current) - targetTime, err2 := time.Parse(timeFormat, target.Format(timeFormat)) // Format target time to same format as current time for proper comparison - timeDiff := targetTime.Sub(currentTime).Abs() - c.log.DEBUG.Printf("current schedule time: %s, target time: %s, time difference: %s", currentTime.Format(timeFormat), targetTime.Format(timeFormat), timeDiff) - - // Round up only if end time changed significantly or if parsing previous time failed - if err1 != nil || err2 != nil || timeDiff > time.Minute { - // round up to next 5 minutes boundary to avoid API rejections - roundedTarget := target.Truncate(minTimeInterval) - if roundedTarget.Before(target) { - roundedTarget = roundedTarget.Add(minTimeInterval) - } - result = roundedTarget.Format(timeFormat) - c.log.DEBUG.Printf("target time %s rounded to %s to fit API requirements", target.Format(timeFormat), result) +func roundUpTo(d time.Duration, t time.Time) time.Time { + // Round up time to next d boundary + rt := t.Truncate(d) + if !rt.After(t) { + rt = rt.Add(d) } - - return result + return rt } // configureChargeSchedule configures the provided schedule with the provided start and end time, while ensuring it fits API requirements and avoiding unnecessary changes if times are not significantly different to prevent API rejections for unchanged schedules. It returns true if the schedule was changed and false otherwise. func (c *Controller) configureChargeSchedule(schedule *Schedule, start time.Time, end time.Time) bool { const ( - timeFormat = "15:04" // Hours & minutes only - defaultEndTime = "23:55" // Default end time to use when enabling charge to avoid API rejections for schedules without end time; set to end of the day to avoid undesired charge stop in the future - fallbackStartTime = "00:00" // Fallback time for schedules crossing midnight + minTimeInterval = 5 * time.Minute // Minimum time interval accepted by Fiat API in schedules; used for rounding up start and end time to avoid API rejections + timeFormat = "15:04" // Hours & minutes only + defaultEndTime = "23:55" // Default end time to use when enabling charge; this is the last time of the day accepted by the Fiat API + fallbackStartTime = "00:05" // Fallback time for schedules crossing midnight; this is the first time of the day accepted by the Fiat API after the risky midnight ) hasChanged := false // track if we made any change to the schedule to avoid unnecessary API calls @@ -133,7 +121,8 @@ func (c *Controller) configureChargeSchedule(schedule *Schedule, start time.Time // Update start only if provided (non-zero) if !start.IsZero() { - newStartStr := c.computeNewApiScheduleTime(schedule.StartTime, start, timeFormat) + // round up to next 5 minutes boundary to avoid API rejections and make sure the schedule will be applied by the vehicle + newStartStr := roundUpTo(minTimeInterval, start).Format(timeFormat) // Update only if different from current if newStartStr != schedule.StartTime { @@ -156,7 +145,8 @@ func (c *Controller) configureChargeSchedule(schedule *Schedule, start time.Time // Update end only if provided (non-zero) if !end.IsZero() { - newEndStr := c.computeNewApiScheduleTime(schedule.EndTime, end, timeFormat) + // round up to next 5 minutes boundary to avoid API rejections and make sure the schedule will be applied by the vehicle + newEndStr := roundUpTo(minTimeInterval, end).Format(timeFormat) // Update only if different from current if newEndStr != schedule.EndTime { @@ -194,7 +184,7 @@ func (c *Controller) disableConflictingChargeSchedule(schedule *Schedule) bool { // make sure the other charge schedules are disabled in case user changed them if schedule.ScheduleType == "CHARGE" && schedule.EnableScheduleType { schedule.EnableScheduleType = false - c.log.INFO.Printf("disabled charge schedule other than the first one to avoid conflicts") + c.log.DEBUG.Printf("disabled charge schedule other than the first one to avoid conflicts") return true // schedule was changed } return false // schedule was not changed @@ -208,13 +198,19 @@ func (c *Controller) WakeUp() error { return nil } - // Trigger deep refresh to wake up the vehicle, and this requires the same pin authentication as other actions - res, err := c.api.DeepRefresh(c.vin, c.pin) - if err != nil && res.ResponseStatus == "pending" { - c.log.DEBUG.Printf("vehicle wakeup triggered successfully with deep refresh action") + // Only call ChargeNow to WakeUp if the charger is handling the charging and not the vehicle, otherwise schedules will not work properly. + if !c.isChargedControlled { + res, err := c.api.ChargeNow(c.vin, c.pin) + if err != nil { + return fmt.Errorf("charge now call failed: %w", err) + } + if res.ResponseStatus != "pending" { + return fmt.Errorf("invalid response status: %s", res.ResponseStatus) + } + c.log.DEBUG.Printf("vehicle wakeup triggered successfully with charge now action") } else { - err = fmt.Errorf("invalid response status: %s", res.ResponseStatus) + c.log.DEBUG.Printf("vehicle wakeup skipped because charge is controlled by evcc, to avoid conflicts with schedules") } - return err + return nil } From e44139d377c4bf41b16ecaeb95eabfbba4ac4a92 Mon Sep 17 00:00:00 2001 From: FraBoch <19388397+FraBoCH@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:07:15 +0100 Subject: [PATCH 17/17] Handle comments from ai review & lint --- vehicle/fiat/controller.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/vehicle/fiat/controller.go b/vehicle/fiat/controller.go index 611cc063a6c..517d2233cac 100644 --- a/vehicle/fiat/controller.go +++ b/vehicle/fiat/controller.go @@ -62,10 +62,10 @@ func (c *Controller) ChargeEnable(enable bool) error { if enable { // Start charging from now until end of day (23:55) - hasChanged = c.configureChargeSchedule(&stat.EvInfo.Schedules[0], now, time.Time{}) + hasChanged = hasChanged || c.configureChargeSchedule(&stat.EvInfo.Schedules[0], now, time.Time{}) } else { - // Stop charging: set charge end time to now to stop charging immediately (use empty time to keep start time as it was for history in Fiat app) - hasChanged = c.configureChargeSchedule(&stat.EvInfo.Schedules[0], time.Time{}, now) + // Stop charging: set charge end time to now to stop charging as soon as possible (within the next 5 minutes, due to 5-minute rounding; use empty time to keep start time as it was for history in Fiat app) + hasChanged = hasChanged || c.configureChargeSchedule(&stat.EvInfo.Schedules[0], time.Time{}, now) } // make sure the other charge schedules are disabled in case user changed them @@ -103,7 +103,7 @@ func (c *Controller) configureChargeSchedule(schedule *Schedule, start time.Time minTimeInterval = 5 * time.Minute // Minimum time interval accepted by Fiat API in schedules; used for rounding up start and end time to avoid API rejections timeFormat = "15:04" // Hours & minutes only defaultEndTime = "23:55" // Default end time to use when enabling charge; this is the last time of the day accepted by the Fiat API - fallbackStartTime = "00:05" // Fallback time for schedules crossing midnight; this is the first time of the day accepted by the Fiat API after the risky midnight + fallbackStartTime = "00:00" // Fallback time for schedules crossing midnight; this is the first time of the day accepted by the Fiat API ) hasChanged := false // track if we made any change to the schedule to avoid unnecessary API calls @@ -132,7 +132,7 @@ func (c *Controller) configureChargeSchedule(schedule *Schedule, start time.Time c.log.DEBUG.Printf("set charge schedule start: %s with default end time: %s", schedule.StartTime, schedule.EndTime) // only enable for current day to avoid undesired charge start in the future - weekday := time.Now().Weekday() + weekday := start.Weekday() schedule.ScheduledDays.Monday = (weekday == time.Monday) schedule.ScheduledDays.Tuesday = (weekday == time.Tuesday) schedule.ScheduledDays.Wednesday = (weekday == time.Wednesday)