diff --git a/api/plans.go b/api/plans.go index 33ae03b252..5c559963df 100644 --- a/api/plans.go +++ b/api/plans.go @@ -1,17 +1,45 @@ package api -import "time" +import ( + "encoding/json" + "time" +) type RepeatingPlan struct { - Weekdays []int `json:"weekdays"` // 0-6 (Sunday-Saturday) - Time string `json:"time"` // HH:MM - Tz string `json:"tz"` // timezone in IANA format - Soc int `json:"soc"` // target soc - Active bool `json:"active"` // active flag - Precondition int64 `json:"-" todo:"..."` // TODO deprecated + Weekdays []int `json:"weekdays"` // 0-6 (Sunday-Saturday) + Time string `json:"time"` // HH:MM + Tz string `json:"tz"` // timezone in IANA format + Soc int `json:"soc"` // target soc + Active bool `json:"active"` // active flag } type PlanStrategy struct { Continuous bool `json:"continuous"` // force continuous planning Precondition time.Duration `json:"precondition"` // precondition duration in seconds } + +type planStrategy struct { + Continuous bool `json:"continuous"` // force continuous planning + Precondition int64 `json:"precondition"` // precondition duration in seconds +} + +func (ps PlanStrategy) MarshalJSON() ([]byte, error) { + return json.Marshal(planStrategy{ + Continuous: ps.Continuous, + Precondition: int64(ps.Precondition.Seconds()), + }) +} + +func (ps *PlanStrategy) UnmarshalJSON(data []byte) error { + var res planStrategy + if err := json.Unmarshal(data, &res); err != nil { + return err + } + + *ps = PlanStrategy{ + Continuous: res.Continuous, + Precondition: time.Duration(res.Precondition) * time.Second, + } + + return nil +} diff --git a/assets/js/components/ChargingPlans/ChargingPlan.vue b/assets/js/components/ChargingPlans/ChargingPlan.vue index fe3e8809c4..d268ab4e25 100644 --- a/assets/js/components/ChargingPlans/ChargingPlan.vue +++ b/assets/js/components/ChargingPlans/ChargingPlan.vue @@ -121,8 +121,7 @@ export default defineComponent({ effectiveLimitSoc: Number, effectivePlanSoc: Number, effectivePlanTime: String, - effectivePlanPrecondition: Number, - effectivePlanContinuous: Boolean, + effectivePlanStrategy: Object as PropType, id: [String, Number], limitEnergy: Number, mode: String, diff --git a/assets/js/components/ChargingPlans/PlansRepeatingSettings.vue b/assets/js/components/ChargingPlans/PlansRepeatingSettings.vue index 4162830aca..ba25e85622 100644 --- a/assets/js/components/ChargingPlans/PlansRepeatingSettings.vue +++ b/assets/js/components/ChargingPlans/PlansRepeatingSettings.vue @@ -62,7 +62,6 @@ export default defineComponent({ weekdays: DEFAULT_WEEKDAYS, time: DEFAULT_TARGET_TIME, soc: DEFAULT_TARGET_SOC, - precondition: 0, active: false, tz: this.timezone(), }; diff --git a/assets/js/components/ChargingPlans/PlansSettings.vue b/assets/js/components/ChargingPlans/PlansSettings.vue index cc5babd797..03b0555085 100644 --- a/assets/js/components/ChargingPlans/PlansSettings.vue +++ b/assets/js/components/ChargingPlans/PlansSettings.vue @@ -56,7 +56,9 @@ , planEnergy: Number, limitEnergy: Number, socBasedPlanning: Boolean, @@ -159,13 +160,6 @@ export default defineComponent({ ? { duration, plan, power, rates, targetTime, currency, smartCostType } : null; }, - chargingPlanStrategyProps(): any { - return { - id: this.id, - precondition: this.effectivePlanPrecondition, - continuous: this.effectivePlanContinuous, - }; - }, alreadyReached(): boolean { return this.plan.duration === 0; }, @@ -186,11 +180,11 @@ export default defineComponent({ this.updatePlanDebounced(); } }, - effectivePlanPrecondition() { - this.updatePlanDebounced(); - }, - effectivePlanContinuous() { - this.updatePlanDebounced(); + effectivePlanStrategy: { + deep: true, + handler() { + this.updatePlanDebounced(); + }, }, staticPlan: { deep: true, diff --git a/assets/js/components/Loadpoints/Loadpoint.stories.ts b/assets/js/components/Loadpoints/Loadpoint.stories.ts index 08238fad5e..bd42924993 100644 --- a/assets/js/components/Loadpoints/Loadpoint.stories.ts +++ b/assets/js/components/Loadpoints/Loadpoint.stories.ts @@ -51,8 +51,7 @@ const baseState = { planActive: false, planEnergy: 0, planOverrun: 0, - planPrecondition: 0, - planContinuous: false, + planStrategy: { continuous: false, precondition: 0 }, planProjectedEnd: undefined, planProjectedStart: undefined, planTime: undefined, @@ -83,8 +82,10 @@ const baseState = { capacity: 8, features: ["Offline"], repeatingPlans: [], - planPrecondition: 0, - planContinuous: false, + planStrategy: { + continuous: false, + precondition: 0, + }, }, { name: "vehicle_4", @@ -93,8 +94,10 @@ const baseState = { capacity: 80, features: ["Offline"], repeatingPlans: [], - planPrecondition: 0, - planContinuous: false, + planStrategy: { + continuous: true, + precondition: 0, + }, }, ], smartCostType: SMART_COST_TYPE.PRICE_FORECAST, diff --git a/assets/js/components/Loadpoints/Loadpoint.vue b/assets/js/components/Loadpoints/Loadpoint.vue index 2106966a13..822d10c67e 100644 --- a/assets/js/components/Loadpoints/Loadpoint.vue +++ b/assets/js/components/Loadpoints/Loadpoint.vue @@ -127,6 +127,7 @@ import type { Forecast, SMART_COST_TYPE, } from "@/types/evcc"; +import type { PlanStrategy } from "@/components/ChargingPlans/types"; export default defineComponent({ name: "Loadpoint", @@ -189,8 +190,7 @@ export default defineComponent({ planTime: String as PropType, effectivePlanTime: String as PropType, effectivePlanSoc: Number, - effectivePlanPrecondition: Number, - effectivePlanContinuous: Boolean, + effectivePlanStrategy: Object as PropType, vehicleProviderLoggedIn: Boolean, vehicleProviderLoginPath: String, vehicleProviderLogoutPath: String, diff --git a/assets/js/components/Loadpoints/Loadpoints.stories.ts b/assets/js/components/Loadpoints/Loadpoints.stories.ts index 1137b279c6..e15a588cf5 100644 --- a/assets/js/components/Loadpoints/Loadpoints.stories.ts +++ b/assets/js/components/Loadpoints/Loadpoints.stories.ts @@ -48,8 +48,10 @@ const baseArgs = { capacity: 8, features: ["Offline"], repeatingPlans: [], - planPrecondition: 0, - planContinuous: false, + planStrategy: { + continuous: false, + precondition: 0, + }, }, { name: "vehicle_4", @@ -58,8 +60,10 @@ const baseArgs = { capacity: 80, features: ["Offline"], repeatingPlans: [], - planPrecondition: 0, - planContinuous: false, + planStrategy: { + continuous: true, + precondition: 0, + }, }, { name: "vehicle_5", @@ -68,8 +72,10 @@ const baseArgs = { capacity: 0.46, features: ["Offline"], repeatingPlans: [], - planPrecondition: 0, - planContinuous: false, + planStrategy: { + continuous: false, + precondition: 0, + }, }, ], smartCostType: SMART_COST_TYPE.PRICE_FORECAST, diff --git a/assets/js/components/Vehicles/Vehicle.stories.ts b/assets/js/components/Vehicles/Vehicle.stories.ts index d26fa01118..00081595b8 100644 --- a/assets/js/components/Vehicles/Vehicle.stories.ts +++ b/assets/js/components/Vehicles/Vehicle.stories.ts @@ -10,8 +10,10 @@ const baseState = { features: [], name: "", repeatingPlans: [], - planPrecondition: 0, - planContinuous: false, + planStrategy: { + continuous: false, + precondition: 0, + }, }, enabled: false, connected: true, diff --git a/assets/js/components/Vehicles/Vehicle.vue b/assets/js/components/Vehicles/Vehicle.vue index acd160a695..24e69056ba 100644 --- a/assets/js/components/Vehicles/Vehicle.vue +++ b/assets/js/components/Vehicles/Vehicle.vue @@ -80,6 +80,7 @@ import LimitEnergySelect from "./LimitEnergySelect.vue"; import { distanceUnit, distanceValue } from "@/units.ts"; import { defineComponent, type PropType } from "vue"; import { CHARGE_MODE, type Forecast, type Vehicle } from "@/types/evcc"; +import type { PlanStrategy } from "@/components/ChargingPlans/types"; export default defineComponent({ name: "Vehicle", @@ -103,8 +104,7 @@ export default defineComponent({ effectiveLimitSoc: Number, effectivePlanSoc: Number, effectivePlanTime: String, - effectivePlanPrecondition: Number, - effectivePlanContinuous: Boolean, + effectivePlanStrategy: Object as PropType, batteryBoostActive: Boolean, enabled: Boolean, heating: Boolean, diff --git a/assets/js/types/evcc.ts b/assets/js/types/evcc.ts index c6714a5cc3..6d655d0298 100644 --- a/assets/js/types/evcc.ts +++ b/assets/js/types/evcc.ts @@ -1,4 +1,4 @@ -import type { StaticPlan, RepeatingPlan } from "../components/ChargingPlans/types"; +import type { StaticPlan, RepeatingPlan, PlanStrategy } from "../components/ChargingPlans/types"; import type { ForecastSlot, SolarDetails } from "../components/Forecast/types"; // react-native-webview @@ -191,7 +191,7 @@ export interface ConfigLoadpoint { smartCostLimit: number | null; planEnergy?: number; planTime?: string; - planPrecondition?: number; + planStrategy?: PlanStrategy; limitEnergy?: number; limitSoc?: number; circuit?: string; @@ -246,8 +246,7 @@ export interface Loadpoint { effectivePlanId: number; effectivePlanSoc: number; effectivePlanTime: string | null; - effectivePlanPrecondition: number; - effectivePlanContinuous: boolean; + effectivePlanStrategy: PlanStrategy; effectivePriority: number; enableDelay: number; enableThreshold: number; @@ -265,8 +264,7 @@ export interface Loadpoint { planActive: boolean; planEnergy: number; planOverrun: number; - planPrecondition: number; - planContinuous: boolean; + planStrategy: PlanStrategy; planProjectedEnd: string | null; planProjectedStart: string | null; planTime: string | null; @@ -481,8 +479,7 @@ export interface Vehicle { limitSoc?: number; plan?: StaticPlan; repeatingPlans: RepeatingPlan[] | null; - planPrecondition: number; - planContinuous: boolean; + planStrategy: PlanStrategy; title: string; features?: string[]; capacity?: number; diff --git a/core/keys/loadpoint.go b/core/keys/loadpoint.go index 7b6598515a..cf2e3a221a 100644 --- a/core/keys/loadpoint.go +++ b/core/keys/loadpoint.go @@ -58,10 +58,8 @@ const ( EffectiveMinCurrent = "effectiveMinCurrent" // effective min current EffectiveMaxCurrent = "effectiveMaxCurrent" // effective max current - EffectiveLimitSoc = "effectiveLimitSoc" // effective limit soc - EffectivePlanStrategy = "effectivePlanStrategy" // effective plan strategy (deprecated, use individual fields) - EffectivePlanPrecondition = "effectivePlanPrecondition" // effective plan precondition duration - EffectivePlanContinuous = "effectivePlanContinuous" // effective plan continuous planning + EffectiveLimitSoc = "effectiveLimitSoc" // effective limit soc + EffectivePlanStrategy = "effectivePlanStrategy" // effective plan strategy // measurements ChargePower = "chargePower" // charge power @@ -86,8 +84,6 @@ const ( PlanProjectedEnd = "planProjectedEnd" // charge plan ends (end of last slot) PlanOverrun = "planOverrun" // charge plan goal not reachable in time PlanStrategy = "planStrategy" // charge plan strategy (precondition, continuous) - PlanPrecondition = "planPrecondition" // charge plan precondition duration - PlanContinuous = "planContinuous" // charge plan continuous planning // repeating plans RepeatingPlans = "repeatingPlans" // key to access all repeating plans in db diff --git a/core/loadpoint.go b/core/loadpoint.go index 6efca15e42..a1c409edc3 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -686,8 +686,7 @@ func (lp *Loadpoint) Prepare(site site.API, uiChan chan<- util.Param, pushChan c // restored settings lp.publish(keys.PlanTime, lp.planTime) lp.publish(keys.PlanEnergy, lp.planEnergy) - lp.publish(keys.PlanPrecondition, int64(lp.planStrategy.Precondition.Seconds())) - lp.publish(keys.PlanContinuous, lp.planStrategy.Continuous) + lp.publish(keys.PlanStrategy, lp.planStrategy) lp.publish(keys.LimitSoc, lp.limitSoc) lp.publish(keys.LimitEnergy, lp.limitEnergy) diff --git a/core/loadpoint_api.go b/core/loadpoint_api.go index 4eb9e8eac5..8e694b25c1 100644 --- a/core/loadpoint_api.go +++ b/core/loadpoint_api.go @@ -402,8 +402,7 @@ func (lp *Loadpoint) setPlanStrategy(strategy api.PlanStrategy) error { } lp.planStrategy = strategy - lp.publish(keys.PlanPrecondition, int64(strategy.Precondition.Seconds())) - lp.publish(keys.PlanContinuous, strategy.Continuous) + lp.publish(keys.PlanStrategy, strategy) lp.requestUpdate() @@ -417,7 +416,14 @@ func (lp *Loadpoint) SetPlanStrategy(strategy api.PlanStrategy) error { lp.log.DEBUG.Printf("set plan strategy: continuous=%v, precondition=%v", strategy.Continuous, strategy.Precondition) - return lp.setPlanStrategy(strategy) + if err := lp.setPlanStrategy(strategy); err != nil { + return err + } + + // publish effective plan strategy immediately (we're already holding Lock) + lp.publish(keys.EffectivePlanStrategy, lp.getEffectivePlanStrategy()) + + return nil } // getPlanStrategy returns the plan strategy (no mutex) diff --git a/core/loadpoint_effective.go b/core/loadpoint_effective.go index 49d1b74612..c0d82ff29c 100644 --- a/core/loadpoint_effective.go +++ b/core/loadpoint_effective.go @@ -12,16 +12,14 @@ import ( // PublishEffectiveValues publishes all effective values func (lp *Loadpoint) PublishEffectiveValues() { - strategy := lp.EffectivePlanStrategy() lp.publish(keys.EffectivePriority, lp.EffectivePriority()) lp.publish(keys.EffectivePlanId, lp.EffectivePlanId()) lp.publish(keys.EffectivePlanTime, lp.EffectivePlanTime()) lp.publish(keys.EffectivePlanSoc, lp.EffectivePlanSoc()) + lp.publish(keys.EffectivePlanStrategy, lp.EffectivePlanStrategy()) lp.publish(keys.EffectiveMinCurrent, lp.effectiveMinCurrent()) lp.publish(keys.EffectiveMaxCurrent, lp.effectiveMaxCurrent()) lp.publish(keys.EffectiveLimitSoc, lp.EffectiveLimitSoc()) - lp.publish(keys.EffectivePlanPrecondition, int64(strategy.Precondition.Seconds())) - lp.publish(keys.EffectivePlanContinuous, strategy.Continuous) } // EffectivePriority returns the effective priority diff --git a/core/site_vehicles.go b/core/site_vehicles.go index f4fc55215f..e9ab609b1a 100644 --- a/core/site_vehicles.go +++ b/core/site_vehicles.go @@ -13,27 +13,24 @@ import ( ) type planStruct struct { - Soc int `json:"soc"` - Continuous bool `json:"continuous"` - Precondition int64 `json:"precondition"` - Time time.Time `json:"time"` + Soc int `json:"soc"` + Time time.Time `json:"time"` } type vehicleStruct struct { - Title string `json:"title"` - Icon string `json:"icon,omitempty"` - Capacity float64 `json:"capacity,omitempty"` - Phases int `json:"phases,omitempty"` - MinSoc int `json:"minSoc,omitempty"` - LimitSoc int `json:"limitSoc,omitempty"` - MinCurrent float64 `json:"minCurrent,omitempty"` - MaxCurrent float64 `json:"maxCurrent,omitempty"` - Priority int `json:"priority,omitempty"` - Features []string `json:"features,omitempty"` - Plan *planStruct `json:"plan,omitempty"` - RepeatingPlans []api.RepeatingPlan `json:"repeatingPlans"` - PlanPrecondition int64 `json:"planPrecondition"` - PlanContinuous bool `json:"planContinuous"` + Title string `json:"title"` + Icon string `json:"icon,omitempty"` + Capacity float64 `json:"capacity,omitempty"` + Phases int `json:"phases,omitempty"` + MinSoc int `json:"minSoc,omitempty"` + LimitSoc int `json:"limitSoc,omitempty"` + MinCurrent float64 `json:"minCurrent,omitempty"` + MaxCurrent float64 `json:"maxCurrent,omitempty"` + Priority int `json:"priority,omitempty"` + Features []string `json:"features,omitempty"` + Plan *planStruct `json:"plan,omitempty"` + RepeatingPlans []api.RepeatingPlan `json:"repeatingPlans"` + PlanStrategy api.PlanStrategy `json:"planStrategy"` } // publishVehicles returns a list of vehicle titles @@ -48,32 +45,34 @@ func (site *Site) publishVehicles() { } ac := instance.OnIdentified() - strategy := v.GetPlanStrategy() var plan *planStruct if time, soc := v.GetPlanSoc(); !time.IsZero() { - plan = &planStruct{Soc: soc, Precondition: int64(strategy.Precondition.Seconds()), Time: time} + plan = &planStruct{ + Soc: soc, + Time: time, + } } res[v.Name()] = vehicleStruct{ - Title: instance.GetTitle(), - Icon: instance.Icon(), - Capacity: instance.Capacity(), - Phases: instance.Phases(), - MinSoc: v.GetMinSoc(), - LimitSoc: v.GetLimitSoc(), - MinCurrent: ac.MinCurrent, - MaxCurrent: ac.MaxCurrent, - Priority: ac.Priority, - Features: lo.Map(instance.Features(), func(f api.Feature, _ int) string { return f.String() }), - Plan: plan, - RepeatingPlans: v.GetRepeatingPlans(), - PlanPrecondition: int64(strategy.Precondition.Seconds()), - PlanContinuous: strategy.Continuous, + Title: instance.GetTitle(), + Icon: instance.Icon(), + Capacity: instance.Capacity(), + Phases: instance.Phases(), + MinSoc: v.GetMinSoc(), + LimitSoc: v.GetLimitSoc(), + MinCurrent: ac.MinCurrent, + MaxCurrent: ac.MaxCurrent, + Priority: ac.Priority, + Features: lo.Map(instance.Features(), func(f api.Feature, _ int) string { return f.String() }), + Plan: plan, + RepeatingPlans: v.GetRepeatingPlans(), + PlanStrategy: v.GetPlanStrategy(), } + // publish effective plan strategy immediately for soc-based planning if lp := site.coordinator.Owner(instance); lp != nil { - go lp.PublishEffectiveValues() + lp.PublishEffectiveValues() } } diff --git a/core/vehicle/adapter.go b/core/vehicle/adapter.go index 40fbaf5eb8..f7a1bf6b9c 100644 --- a/core/vehicle/adapter.go +++ b/core/vehicle/adapter.go @@ -153,7 +153,6 @@ func (v *adapter) SetPlanStrategy(planStrategy api.PlanStrategy) error { return err } - v.log.DEBUG.Printf("update plan strategy for vehicle %s (precondition: %vs, continuous: %v)", v.name, planStrategy.Continuous, planStrategy.Precondition) v.publish() return nil diff --git a/server/http_loadpoint_handler.go b/server/http_loadpoint_handler.go index 92fbf7d03e..3012b252ee 100644 --- a/server/http_loadpoint_handler.go +++ b/server/http_loadpoint_handler.go @@ -26,10 +26,6 @@ type PlanPreviewResponse struct { Duration int64 `json:"duration"` Plan api.Rates `json:"plan"` Power float64 `json:"power"` - - // TODO remove - Continuous bool `json:"continuous"` - Precondition int64 `json:"precondition"` } // planHandler returns the current plan @@ -100,10 +96,6 @@ func staticPlanPreviewHandler(lp loadpoint.API) http.HandlerFunc { Duration: int64(requiredDuration.Seconds()), Plan: plan, Power: maxPower, - - // TODO remove - Continuous: strategy.Continuous, - Precondition: int64(strategy.Precondition.Seconds()), } jsonWrite(w, res) diff --git a/tariff/solcast/types.go b/tariff/solcast/types.go index a37af2efb9..c878f081c4 100644 --- a/tariff/solcast/types.go +++ b/tariff/solcast/types.go @@ -24,8 +24,8 @@ func (d *Duration) Duration() time.Duration { return time.Duration(*d) } -func (d *Duration) UnmarshalJSON(b []byte) error { - val, err := iso8601.ParseDuration(string(b)) +func (d *Duration) UnmarshalJSON(data []byte) error { + val, err := iso8601.ParseDuration(string(data)) if err != nil { return err } diff --git a/util/shortrfc3339/shortrfc3339.go b/util/shortrfc3339/shortrfc3339.go index 7de6935635..b1912dc7fa 100644 --- a/util/shortrfc3339/shortrfc3339.go +++ b/util/shortrfc3339/shortrfc3339.go @@ -15,8 +15,8 @@ type Timestamp struct { // Layout is the time.Parse compliant parsing string for use when parsing Shortened RFC-3339 compliant timestamps. const Layout = "2006-01-02T15:04Z" -func (ct *Timestamp) UnmarshalJSON(b []byte) (err error) { - s := strings.Trim(string(b), "\"") +func (ct *Timestamp) UnmarshalJSON(data []byte) (err error) { + s := strings.Trim(string(data), "\"") if s == "null" { ct.Time = time.Time{} return diff --git a/vehicle/psa/duration.go b/vehicle/psa/duration.go index 570a1885cb..334c1374b2 100644 --- a/vehicle/psa/duration.go +++ b/vehicle/psa/duration.go @@ -14,9 +14,9 @@ type Duration struct { } // UnmarshalJSON implements json.Unmarshaler -func (d *Duration) UnmarshalJSON(b []byte) error { +func (d *Duration) UnmarshalJSON(data []byte) error { var v any - if err := json.Unmarshal(b, &v); err != nil { + if err := json.Unmarshal(data, &v); err != nil { return err }