265 lines
6.7 KiB
Go
265 lines
6.7 KiB
Go
package charger
|
|
|
|
// LICENSE
|
|
|
|
// Copyright (c) evcc.io (andig, naltatis, premultiply)
|
|
|
|
// This module is NOT covered by the MIT license. All rights reserved.
|
|
|
|
// The above copyright notice and this permission notice shall be included in all
|
|
// copies or substantial portions of the Software.
|
|
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
// SOFTWARE.
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/WulfgarW/sensonet"
|
|
"github.com/evcc-io/evcc/api"
|
|
"github.com/evcc-io/evcc/util"
|
|
"github.com/evcc-io/evcc/util/request"
|
|
"github.com/samber/lo"
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
func init() {
|
|
registry.AddCtx("vaillant", NewVaillantFromConfig)
|
|
}
|
|
|
|
type Vaillant struct {
|
|
*SgReady
|
|
log *util.Logger
|
|
conn *sensonet.Connection
|
|
systemId string
|
|
}
|
|
|
|
//go:generate go tool decorate -f decorateVaillant -b *Vaillant -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.Battery,Soc,func() (float64, error)"
|
|
|
|
// NewVaillantFromConfig creates an Vaillant configurable charger from generic config
|
|
func NewVaillantFromConfig(ctx context.Context, other map[string]any) (api.Charger, error) {
|
|
cc := struct {
|
|
embed `mapstructure:",squash"`
|
|
User, Password string
|
|
Realm string
|
|
System string
|
|
HeatingZone int
|
|
HeatingSetpoint float32
|
|
Cache time.Duration
|
|
}{
|
|
embed: embed{
|
|
Icon_: "heatpump",
|
|
Features_: []api.Feature{api.Heating, api.IntegratedDevice},
|
|
},
|
|
Realm: sensonet.REALM_GERMANY,
|
|
Cache: time.Minute,
|
|
}
|
|
|
|
if err := util.DecodeOther(other, &cc); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if cc.User == "" || cc.Password == "" {
|
|
return nil, api.ErrMissingCredentials
|
|
}
|
|
|
|
log := util.NewLogger("vaillant").Redact(cc.User, cc.Password)
|
|
logCtx := context.WithValue(ctx, oauth2.HTTPClient, request.NewClient(log))
|
|
|
|
oc := sensonet.Oauth2ConfigForRealm(cc.Realm)
|
|
token, err := oc.PasswordCredentialsToken(logCtx, cc.User, cc.Password)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
conn, err := sensonet.NewConnection(oc.TokenSource(logCtx, token), sensonet.WithHttpClient(request.NewClient(log)))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
home, err := ensureEx("home", cc.System, func() ([]sensonet.Home, error) {
|
|
return conn.GetHomes()
|
|
}, func(home sensonet.Home) (string, error) {
|
|
return home.SystemID, nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
systemId := home.SystemID
|
|
heating := cc.HeatingSetpoint > 0
|
|
|
|
wwCancel := func() {}
|
|
|
|
set := func(mode int64) error {
|
|
switch mode {
|
|
case Normal:
|
|
if heating {
|
|
return conn.StopZoneQuickVeto(systemId, cc.HeatingZone)
|
|
}
|
|
|
|
wwCancel()
|
|
return conn.StopHotWaterBoost(systemId, sensonet.HOTWATERINDEX_DEFAULT)
|
|
|
|
case Boost:
|
|
if heating {
|
|
return conn.StartZoneQuickVeto(systemId, cc.HeatingZone, cc.HeatingSetpoint, 4) // hours
|
|
}
|
|
|
|
if err := conn.StartHotWaterBoost(systemId, sensonet.HOTWATERINDEX_DEFAULT); err != nil {
|
|
return err
|
|
}
|
|
|
|
var wwCtx context.Context
|
|
wwCtx, wwCancel = context.WithCancel(ctx)
|
|
|
|
// re-boost every 15m
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-wwCtx.Done():
|
|
return
|
|
case <-time.After(15 * time.Minute):
|
|
if err := conn.StartHotWaterBoost(systemId, sensonet.HOTWATERINDEX_DEFAULT); err != nil {
|
|
log.ERROR.Println("hot water boost:", err)
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
|
|
default:
|
|
return api.ErrNotAvailable
|
|
}
|
|
}
|
|
|
|
sgr, err := NewSgReady(ctx, &cc.embed, set, nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res := &Vaillant{
|
|
log: log,
|
|
conn: conn,
|
|
systemId: systemId,
|
|
SgReady: sgr,
|
|
}
|
|
|
|
var power func() (float64, error)
|
|
if devices, _ := conn.GetMpcData(systemId); len(devices) > 0 {
|
|
power = util.Cached(func() (float64, error) {
|
|
res, err := conn.GetMpcData(systemId)
|
|
return lo.SumBy(res, func(d sensonet.MpcDevice) float64 {
|
|
return d.CurrentPower
|
|
}), err
|
|
}, cc.Cache)
|
|
}
|
|
|
|
heatingTemp := func(zz []sensonet.StateZone) float64 {
|
|
z, _ := lo.Find(zz, func(z sensonet.StateZone) bool {
|
|
return z.Index == cc.HeatingZone
|
|
})
|
|
return z.CurrentRoomTemperature
|
|
}
|
|
|
|
var heatingTempSensor bool
|
|
if heating {
|
|
system, err := conn.GetSystem(systemId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
heatingTempSensor = heatingTemp(system.State.Zones) > 0
|
|
}
|
|
|
|
var temp func() (float64, error)
|
|
if !heating || heatingTempSensor {
|
|
temp = util.Cached(func() (float64, error) {
|
|
system, err := conn.GetSystem(systemId)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
switch {
|
|
case heatingTempSensor:
|
|
if res := heatingTemp(system.State.Zones); res > 0 {
|
|
return res, nil
|
|
}
|
|
return 0, api.ErrNotAvailable
|
|
case len(system.State.Dhw) > 0:
|
|
return system.State.Dhw[0].CurrentDhwTemperature, nil
|
|
case len(system.State.DomesticHotWater) > 0:
|
|
return system.State.DomesticHotWater[0].CurrentDomesticHotWaterTemperature, nil
|
|
default:
|
|
return 0, api.ErrNotAvailable
|
|
}
|
|
}, cc.Cache)
|
|
}
|
|
|
|
return decorateVaillant(res, power, temp), nil
|
|
}
|
|
|
|
func (v *Vaillant) print(chapter int, prefix string, zz ...any) {
|
|
var i int
|
|
for _, z := range zz {
|
|
rt := reflect.TypeOf(z)
|
|
rv := reflect.ValueOf(z)
|
|
|
|
if rt.Kind() == reflect.Slice && rv.Len() == 0 {
|
|
continue
|
|
}
|
|
i++
|
|
|
|
typ := strings.TrimPrefix(strings.TrimPrefix(fmt.Sprintf("%T", z), "[]sensonet."), prefix)
|
|
|
|
fmt.Println()
|
|
fmt.Printf("%d.%d. %s\n", chapter, i+1, typ)
|
|
|
|
if rt.Kind() == reflect.Slice {
|
|
for j := range rv.Len() {
|
|
fmt.Printf("%d.%d.%d. %s %d\n", chapter, i+1, j+1, typ, j)
|
|
fmt.Printf("%+v\n", rv.Index(j))
|
|
}
|
|
} else {
|
|
fmt.Printf("%+v\n", z)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (v *Vaillant) Diagnose() {
|
|
sys, err := v.conn.GetSystem(v.systemId)
|
|
if err != nil {
|
|
v.log.ERROR.Println(err)
|
|
return
|
|
}
|
|
|
|
fmt.Println()
|
|
fmt.Println("1. State")
|
|
fmt.Println()
|
|
fmt.Println("1.1. System")
|
|
fmt.Printf("%+v\n", sys.State.System)
|
|
v.print(1, "State", sys.State.Zones, sys.State.Circuits, sys.State.Dhw, sys.State.DomesticHotWater)
|
|
|
|
fmt.Println()
|
|
fmt.Println("2. Properties")
|
|
fmt.Println()
|
|
fmt.Println("2.1. System")
|
|
fmt.Printf("%+v\n", sys.Properties.System)
|
|
v.print(2, "Properties", sys.Properties.Zones, sys.Properties.Circuits, sys.Properties.Dhw, sys.Properties.DomesticHotWater)
|
|
|
|
fmt.Println()
|
|
fmt.Println("3. Configuration")
|
|
fmt.Println()
|
|
fmt.Println("3.1. System")
|
|
fmt.Printf("%+v\n", sys.Configuration.System)
|
|
v.print(3, "Configuration", sys.Configuration.Zones, sys.Configuration.Circuits, sys.Configuration.Dhw, sys.Configuration.DomesticHotWater)
|
|
}
|