From f11cf690dc2f7fc243a6a67873a39eb61d93ef74 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 4 Jul 2026 10:23:37 +0200 Subject: [PATCH] Vaillant: make hysteresis and reboost interval configurable (#31379) --- charger/vaillant.go | 77 ++++++++++++++++++---- templates/definition/charger/vaillant.yaml | 19 ++++++ 2 files changed, 83 insertions(+), 13 deletions(-) diff --git a/charger/vaillant.go b/charger/vaillant.go index f477ee9b9..69971e7dc 100644 --- a/charger/vaillant.go +++ b/charger/vaillant.go @@ -19,6 +19,7 @@ package charger import ( "context" + "errors" "fmt" "reflect" "strings" @@ -27,6 +28,7 @@ import ( "github.com/WulfgarW/sensonet" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/api/implement" + "github.com/evcc-io/evcc/core/loadpoint" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" "github.com/samber/lo" @@ -41,6 +43,7 @@ type Vaillant struct { *SgReady log *util.Logger conn *sensonet.Connection + lp loadpoint.API systemId string } @@ -53,14 +56,17 @@ func NewVaillantFromConfig(ctx context.Context, other map[string]any) (api.Charg System string HeatingZone int HeatingSetpoint float32 + Hysteresis float64 + Reboost time.Duration Cache time.Duration }{ embed: embed{ Icon_: "heatpump", Features_: []api.Feature{api.Continuous, api.Heating, api.IntegratedDevice, api.SwitchDevice}, }, - Realm: sensonet.REALM_GERMANY, - Cache: time.Minute, + Realm: sensonet.REALM_GERMANY, + Reboost: 15 * time.Minute, + Cache: time.Minute, } if err := util.DecodeOther(other, &cc); err != nil { @@ -99,6 +105,45 @@ func NewVaillantFromConfig(ctx context.Context, other map[string]any) (api.Charg wwCancel := func() {} + res := &Vaillant{ + log: log, + conn: conn, + systemId: systemId, + } + + // skipBoost reports whether the (re-)boost can be skipped because the measured + // temperature is already within the hysteresis band below the loadpoint limit + skipBoost := func() bool { + if res.lp == nil || cc.Hysteresis <= 0 { + return false + } + + bat, ok := api.Cap[api.Battery](res) + if !ok { + return false + } + + temp, err := bat.Soc() + if err != nil { + if !errors.Is(err, api.ErrNotAvailable) { + log.ERROR.Printf("temp: %v", err) + } + return false + } + + limit := res.lp.GetLimitSoc() + if limit <= 0 { + return false + } + + if hysteresis := float64(limit) - cc.Hysteresis; temp >= hysteresis { + log.DEBUG.Printf("temp: %.1f >= %.1f hysteresis", temp, hysteresis) + return true + } + + return false + } + set := func(mode int64) error { switch mode { case Normal: @@ -114,8 +159,10 @@ func NewVaillantFromConfig(ctx context.Context, other map[string]any) (api.Charg return conn.StartZoneQuickVeto(systemId, cc.HeatingZone, cc.HeatingSetpoint, 4) // hours } - if err := conn.StartHotWaterBoost(systemId, sensonet.HOTWATERINDEX_DEFAULT); err != nil { - return err + if !skipBoost() { + if err := conn.StartHotWaterBoost(systemId, sensonet.HOTWATERINDEX_DEFAULT); err != nil { + return err + } } var wwCtx context.Context @@ -127,7 +174,11 @@ func NewVaillantFromConfig(ctx context.Context, other map[string]any) (api.Charg select { case <-wwCtx.Done(): return - case <-time.After(15 * time.Minute): + case <-time.After(cc.Reboost): + if skipBoost() { + continue + } + if err := conn.StartHotWaterBoost(systemId, sensonet.HOTWATERINDEX_DEFAULT); err != nil { log.ERROR.Println("hot water boost:", err) } @@ -142,18 +193,11 @@ func NewVaillantFromConfig(ctx context.Context, other map[string]any) (api.Charg } } - sgr, err := NewSgReady(ctx, &cc.embed, set, nil, nil) + res.SgReady, err = NewSgReady(ctx, &cc.embed, set, nil, nil) if err != nil { return nil, err } - res := &Vaillant{ - log: log, - conn: conn, - systemId: systemId, - SgReady: sgr, - } - if devices, _ := conn.GetMpcData(systemId); len(devices) > 0 { implement.Has(res, implement.Meter(util.Cached(func() (float64, error) { res, err := conn.GetMpcData(systemId) @@ -205,6 +249,13 @@ func NewVaillantFromConfig(ctx context.Context, other map[string]any) (api.Charg return res, nil } +var _ loadpoint.Controller = (*Vaillant)(nil) + +// LoadpointControl implements loadpoint.Controller +func (wb *Vaillant) LoadpointControl(lp loadpoint.API) { + wb.lp = lp +} + func (v *Vaillant) print(chapter int, prefix string, zz ...any) { var i int for _, z := range zz { diff --git a/templates/definition/charger/vaillant.yaml b/templates/definition/charger/vaillant.yaml index cdf1626f1..658ebdb10 100644 --- a/templates/definition/charger/vaillant.yaml +++ b/templates/definition/charger/vaillant.yaml @@ -78,6 +78,23 @@ params: help: de: Notwendig falls mehrere Anlagen im Account vorhanden sind en: Required if multiple systems are present in the account + - name: reboost + type: duration + default: 15m + advanced: true + description: + de: Reboost-Intervall + en: Reboost interval + help: + de: Wartezeit, nach der Boost erneut gestartet wird. + en: Wait time after which Boost is started again. + - name: hysteresis + type: float + unit: °C + advanced: true + description: + de: Reboost Hysterese + en: Reboost Hysteresis - name: phases deprecated: true render: | @@ -90,3 +107,5 @@ render: | {{- end }} heatingzone: {{ .zone }} heatingsetpoint: {{ .setpoint }} + reboost: {{ .reboost }} + hysteresis: {{ .hysteresis }}