@@ -52,9 +52,8 @@ var _ site.API = (*Site)(nil)
5252
5353// Site is the main configuration container. A site can host multiple loadpoints.
5454type Site struct {
55- valueChan chan <- util.Param // client push messages
56- lpUpdateChan chan * Loadpoint
57- batteryReplanChan chan struct {} // fast loop requests an out-of-band battery re-decision (direction flip)
55+ valueChan chan <- util.Param // client push messages
56+ lpUpdateChan chan * Loadpoint
5857
5958 sync.RWMutex
6059 log * util.Logger
@@ -76,26 +75,30 @@ type Site struct {
7675 consumerMeters []config.Device [api.Meter ] // Consumer meters
7776
7877 // battery settings
79- prioritySoc float64 // prefer battery up to this Soc
80- bufferSoc float64 // continue charging on battery above this Soc (EV loadpoints)
81- bufferStartSoc float64 // start charging on battery above this Soc (EV loadpoints)
82- batteryDischargeControl bool // prevent battery discharge for fast and planned charging
83- batterySolarControl bool // actively charge from surplus / discharge to cover loads
84- batteryCalibrationCharge bool // one-shot: bypass maxSoc and charge to 100% for LFP calibration (not persisted)
85- batteryControlDeadBand float64 // minimum surplus/deficit (W) to start charge or discharge (stability dead band)
86- batteryGridChargeLimit * float64 // grid charging limit
87- batterySolarPool bool // distribute power equally across all batteries (no per-battery selection)
88- batterySolarTiering bool // activate minimum number of batteries needed to stay above inverter's effective power floor
89- batterySolarSticky bool // keep the same battery selection across ticks; swap only on significant SoC divergence
90- batterySolarTapering bool // linearly reduce charge power in the last SoC band before maxSoc to protect cells
91- batteryChargeTier int // tiered activation: current number of batteries charging (0 = uninitialised)
92- batteryDischargeTier int // tiered activation: current number of batteries discharging (0 = uninitialised)
93- batteryChargeActive []string // sticky selection: names of batteries currently in the charge tier
94- batteryDischargeActive []string // sticky selection: names of batteries currently in the discharge tier
95- batteryStopped map [string ]int // ticks since stop was last sent per battery; skips redundant re-stops
96- batteryPlanMu sync.Mutex // guards batteryPlan and serializes main loop battery section vs fast loop
97- batteryPlan * batteryControlPlan // contract between main loop (writes) and fast loop (adjusts)
98- batteryLastDirection batteryPlanDirection // direction of the previous solar control tick; direction changes force fresh power commands
78+ prioritySoc float64 // prefer battery up to this Soc
79+ bufferSoc float64 // continue charging on battery above this Soc (EV loadpoints)
80+ bufferStartSoc float64 // start charging on battery above this Soc (EV loadpoints)
81+ batteryDischargeControl bool // prevent battery discharge for fast and planned charging
82+ batterySolarControl bool // actively charge from surplus / discharge to cover loads
83+ batteryCalibrationCharge bool // one-shot: bypass maxSoc and charge to 100% for LFP calibration (not persisted)
84+ batteryControlDeadBand float64 // minimum surplus/deficit (W) to start charge or discharge (stability dead band)
85+ batteryGridChargeLimit * float64 // grid charging limit
86+ batterySolarPool bool // distribute power equally across all batteries (no per-battery selection)
87+ batterySolarTiering bool // activate minimum number of batteries needed to stay above inverter's effective power floor
88+ batterySolarSticky bool // keep the same battery selection across ticks; swap only on significant SoC divergence
89+ batterySolarTapering bool // linearly reduce charge power in the last SoC band before maxSoc to protect cells
90+ batteryChargeTier int // tiered activation: current number of batteries charging (0 = uninitialised)
91+ batteryDischargeTier int // tiered activation: current number of batteries discharging (0 = uninitialised)
92+ batteryChargeActive []string // sticky selection: names of batteries currently in the charge tier
93+ batteryDischargeActive []string // sticky selection: names of batteries currently in the discharge tier
94+ batteryStopped map [string ]int // ticks since stop was last sent per battery; skips redundant re-stops
95+ batteryPlanMu sync.Mutex // guards batterySnapshot and serializes snapshot build vs fast loop
96+ batterySnapshot * batterySnapshot // main loop → fast loop contract (SoC/limits/caps + config, no power)
97+ batteryFastDirection batteryPlanDirection // direction the fast loop is currently committed to
98+ batteryLastGrid float64 // fast loop meter guard: previous grid reading
99+ batteryLastBatt float64 // fast loop meter guard: previous total battery reading
100+ batteryGuardValid bool // fast loop meter guard: previous readings are valid
101+ batteryFlipSince time.Time // fast loop: when the opposite direction first became wanted
99102
100103 // optimizer settings
101104 optimizerChargingStrategy string // optimizer grid charging strategy
@@ -117,10 +120,8 @@ type Site struct {
117120 batteryMode api.BatteryMode // Battery mode (runtime only, not persisted)
118121 batteryModeExternal api.BatteryMode // Battery mode (external, runtime only, not persisted)
119122 batteryModeExternalTimer time.Time // Battery mode timer for external control
120- lastFlexiblePower float64 // last PV-mode charge flexibility, reused by out-of-band battery replan
121- lastTotalChargePower float64 // last total loadpoint charge power, reused by out-of-band battery replan
122- lastBatteryFlipRequest time.Time // last fast-loop flip poke; adaptive spacing to bound direction thrash
123- batteryFlipBackoff time.Duration // current minimum spacing between flip pokes; grows on rapid re-flips, resets when calm
123+ lastBatteryFlipRequest time.Time // fast loop: last direction flip; adaptive spacing to bound thrash
124+ batteryFlipBackoff time.Duration // fast loop: current minimum spacing between flips; grows on rapid re-flips, resets when calm
124125}
125126
126127// MetersConfig contains the site's meter configuration
@@ -1146,11 +1147,6 @@ func (site *Site) update(lp updater) {
11461147 site .log .WARN .Println ("planner:" , msg )
11471148 }
11481149
1149- // cache loadpoint-derived scalars so an out-of-band battery replan can reuse them
1150- // without re-running the loadpoint cycle (see replanBattery)
1151- site .lastFlexiblePower = flexiblePower
1152- site .lastTotalChargePower = totalChargePower
1153-
11541150 // update battery after reading meters to ensure that (modbus) connection is open
11551151 batteryGridChargeActive := site .batteryGridChargeActive (rate )
11561152 site .publish (keys .BatteryGridChargeActive , batteryGridChargeActive )
@@ -1159,31 +1155,6 @@ func (site *Site) update(lp updater) {
11591155 site .stats .Update (site )
11601156}
11611157
1162- // replanBattery re-runs only the battery direction decision with fresh meter
1163- // readings, without re-running the loadpoint cycle. Triggered out-of-band by the
1164- // fast loop when it detects a charge<->discharge crossing, so a direction flip does
1165- // not wait for the next scheduled tick. Reuses the last loadpoint-derived scalars
1166- // (flexiblePower is irrelevant unless an EV charges in PV mode; totalChargePower is
1167- // unused when grid and PV meters are present). Runs in the Run goroutine, so it never
1168- // overlaps a scheduled update.
1169- func (site * Site ) replanBattery () {
1170- consumption , err := site .tariffRates (api .TariffUsagePlanner )
1171- if err != nil {
1172- site .log .WARN .Println ("planner:" , err )
1173- }
1174-
1175- sitePower , _ , _ , err := site .sitePower (site .lastTotalChargePower , site .lastFlexiblePower )
1176- if err != nil {
1177- site .log .ERROR .Println (err )
1178- return
1179- }
1180-
1181- rate , _ := consumption .At (time .Now ())
1182- batteryGridChargeActive := site .batteryGridChargeActive (rate )
1183- site .publish (keys .BatteryGridChargeActive , batteryGridChargeActive )
1184- site .updateBatteryMode (batteryGridChargeActive , rate , sitePower , true )
1185- }
1186-
11871158// prepare publishes initial values
11881159func (site * Site ) prepare () {
11891160 if err := site .restoreSettings (); err != nil {
@@ -1245,8 +1216,7 @@ func (site *Site) Prepare(valueChan chan<- util.Param, pushChan chan<- messenger
12451216 }
12461217 }()
12471218
1248- site .lpUpdateChan = make (chan * Loadpoint , 1 ) // 1 capacity to avoid deadlock
1249- site .batteryReplanChan = make (chan struct {}, 1 ) // 1 capacity; non-blocking send coalesces requests
1219+ site .lpUpdateChan = make (chan * Loadpoint , 1 ) // 1 capacity to avoid deadlock
12501220
12511221 site .prepare ()
12521222
@@ -1321,8 +1291,6 @@ func (site *Site) Run(stopC chan struct{}, interval time.Duration) {
13211291 site .update (<- loadpointChan )
13221292 case lp := <- site .lpUpdateChan :
13231293 site .update (lp )
1324- case <- site .batteryReplanChan :
1325- site .replanBattery ()
13261294 case <- stopC :
13271295 return
13281296 }
0 commit comments