Add EV charging advice notification to status page
Uses hourly PV forecast data (watts per hour from forecast.solar) to find the first contiguous block of hours where: PV production - base_load_w >= min_surplus_w Shows a blue advice card on the status page: "Auto einstecken bis HH:MM Uhr" "Erwartetes Überschuss-Fenster: HH:MM – HH:MM Uhr" Card is hidden when: - No surplus window found (weak forecast day) - Window has already started or passed - A wallbox is currently active (car already charging) Config: forecast.base_load_w (1000W), forecast.min_surplus_w (1800W) Timestamp parsing handles both "HH:MM:SS" and "HH:MM" key formats. Today: surplus window 11:00–13:00 (24.3 kWh forecast). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -136,6 +136,13 @@ type forecastView struct {
|
||||
Quality string
|
||||
}
|
||||
|
||||
type chargingAdviceView struct {
|
||||
Show bool
|
||||
PlugInBy string // e.g. "11:30"
|
||||
WindowStart string // e.g. "12:00"
|
||||
WindowEnd string // e.g. "16:00"
|
||||
}
|
||||
|
||||
type pageData struct {
|
||||
LastUpdate time.Time
|
||||
ErrMsg string
|
||||
@@ -150,6 +157,7 @@ type pageData struct {
|
||||
HasPhaseData bool
|
||||
Phases []phaseView
|
||||
Forecast forecastView
|
||||
ChargingAdvice chargingAdviceView
|
||||
Consumers []consumerView
|
||||
}
|
||||
|
||||
@@ -253,6 +261,25 @@ func (s *Store) Handler() http.HandlerFunc {
|
||||
Icon: s.fcResult.QualityIcon(s.strategic),
|
||||
Quality: s.fcResult.Quality(s.strategic),
|
||||
}
|
||||
|
||||
// Charging advice: show when surplus window is ahead and no wallbox is active
|
||||
ws := s.fcResult.SurplusWindowStart
|
||||
we := s.fcResult.SurplusWindowEnd
|
||||
now := time.Now()
|
||||
wallboxActive := s.consumers[engine.ConsumerWallboxA].active ||
|
||||
s.consumers[engine.ConsumerWallboxB].active
|
||||
if !ws.IsZero() && now.Before(ws) && !wallboxActive {
|
||||
plugBy := ws.Add(-30 * time.Minute)
|
||||
if plugBy.Before(now) {
|
||||
plugBy = ws // less than 30 min to window — just show window start
|
||||
}
|
||||
data.ChargingAdvice = chargingAdviceView{
|
||||
Show: true,
|
||||
PlugInBy: plugBy.Format("15:04"),
|
||||
WindowStart: ws.Format("15:04"),
|
||||
WindowEnd: we.Format("15:04"),
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, c := range consumerOrder {
|
||||
rec := s.consumers[c]
|
||||
@@ -415,6 +442,33 @@ h1 { font-size: 1.4rem; font-weight: 700; color: #14532d; }
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
|
||||
/* Charging advice card */
|
||||
.advice-card {
|
||||
background: #eff6ff;
|
||||
border: 1px solid #bfdbfe;
|
||||
border-radius: 14px;
|
||||
padding: 0.85rem 1rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.advice-title {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
color: #1d4ed8;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.advice-main {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: #1e3a8a;
|
||||
}
|
||||
.advice-sub {
|
||||
font-size: 0.78rem;
|
||||
color: #3b82f6;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
/* Live power badge on consumers */
|
||||
.power-badge {
|
||||
font-size: 0.72rem;
|
||||
@@ -583,6 +637,16 @@ h1 { font-size: 1.4rem; font-weight: 700; color: #14532d; }
|
||||
|
||||
</div>
|
||||
|
||||
{{if .ChargingAdvice.Show}}
|
||||
<div class="advice-card">
|
||||
<div class="advice-title">🔌 Ladeempfehlung</div>
|
||||
<div class="advice-main">Auto einstecken bis {{.ChargingAdvice.PlugInBy}} Uhr</div>
|
||||
<div class="advice-sub">
|
||||
Erwartetes Überschuss-Fenster: {{.ChargingAdvice.WindowStart}} – {{.ChargingAdvice.WindowEnd}} Uhr
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="section-title">Verbraucher</div>
|
||||
|
||||
{{range .Consumers}}
|
||||
|
||||
Reference in New Issue
Block a user