Add Vaillant SG Ready charger (#17845)

This commit is contained in:
andig 2024-12-27 12:24:39 +01:00 • committed by GitHub
parent 7866f36c7b
commit a92b6eac5e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 305 additions and 15 deletions

224
charger/vaillant.go Normal file
View file

@ -0,0 +1,224 @@
package charger
// LICENSE
// Copyright (c) 2024 andig
// 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"
"errors"
"fmt"
"reflect"
"strings"
"github.com/WulfgarW/sensonet"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
"golang.org/x/oauth2"
)
func init() {
registry.AddCtx("vaillant", NewVaillantFromConfig)
}
type Vaillant struct {
*SgReady
log *util.Logger
conn *sensonet.Connection
systemId string
}
//go:generate decorate -f decorateVaillant -b *Vaillant -r api.Charger -t "api.Battery,Soc,func() (float64, error)"
// NewVaillantFromConfig creates an Vaillant configurable charger from generic config
func NewVaillantFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
cc := struct {
embed `mapstructure:",squash"`
User, Password string
Realm string
HeatingZone int
HeatingSetpoint float32
Phases int
}{
embed: embed{
Icon_: "heatpump",
Features_: []api.Feature{api.Heating, api.IntegratedDevice},
},
Realm: sensonet.REALM_GERMANY,
Phases: 1,
}
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)
client := request.NewClient(log)
clientCtx := context.WithValue(ctx, oauth2.HTTPClient, client)
oc := sensonet.Oauth2ConfigForRealm(cc.Realm)
token, err := oc.PasswordCredentialsToken(clientCtx, cc.User, cc.Password)
if err != nil {
return nil, err
}
conn, err := sensonet.NewConnection(client, oc.TokenSource(clientCtx, token))
if err != nil {
return nil, err
}
homes, err := conn.GetHomes()
if err != nil {
return nil, err
}
systemId := homes[0].SystemID
system, err := conn.GetSystem(systemId)
if err != nil {
return nil, err
}
var strategy int
hotWater := len(system.State.Dhw)+len(system.State.DomesticHotWater) > 0
switch {
case hotWater && cc.HeatingSetpoint != 0:
strategy = sensonet.STRATEGY_HOTWATER_THEN_HEATING
case hotWater:
strategy = sensonet.STRATEGY_HOTWATER
case cc.HeatingSetpoint != 0:
strategy = sensonet.STRATEGY_HEATING
default:
return nil, errors.New("could not determine boost strategy, need either hot water or heating zone and setpoint")
}
heatingPar := sensonet.HeatingParStruct{
ZoneIndex: cc.HeatingZone,
VetoSetpoint: cc.HeatingSetpoint,
VetoDuration: -1, // negative value means: use default
}
hotwaterPar := sensonet.HotwaterParStruct{
Index: -1, // negative value means: use default
}
set := func(mode int64) error {
switch mode {
case Normal:
_, err := conn.StopStrategybased(systemId, &heatingPar, &hotwaterPar)
return err
case Boost:
_, err := conn.StartStrategybased(systemId, strategy, &heatingPar, &hotwaterPar)
return err
default:
return api.ErrNotAvailable
}
}
sgr, err := NewSgReady(ctx, &cc.embed, set, nil, nil, cc.Phases)
if err != nil {
return nil, err
}
res := &Vaillant{
log: log,
conn: conn,
systemId: systemId,
SgReady: sgr,
}
var temp func() (float64, error)
if hotWater {
temp = func() (float64, error) {
system, err := conn.GetSystem(systemId)
if err != nil {
return 0, err
}
switch {
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
}
}
}
return decorateVaillant(res, 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 := 0; j < rv.Len(); j++ {
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)
}

View file

@ -0,0 +1,35 @@
package charger
// Code generated by github.com/evcc-io/evcc/cmd/tools/decorate.go. DO NOT EDIT.
import (
"github.com/evcc-io/evcc/api"
)
func decorateVaillant(base *Vaillant, battery func() (float64, error)) api.Charger {
switch {
case battery == nil:
return base
case battery != nil:
return &struct {
*Vaillant
api.Battery
}{
Vaillant: base,
Battery: &decorateVaillantBatteryImpl{
battery: battery,
},
}
}
return nil
}
type decorateVaillantBatteryImpl struct {
battery func() (float64, error)
}
func (impl *decorateVaillantBatteryImpl) Soc() (float64, error) {
return impl.battery()
}

11
go.mod
View file

@ -8,6 +8,7 @@ require (
github.com/BurntSushi/toml v1.4.0
github.com/Masterminds/sprig/v3 v3.3.0
github.com/PuerkitoBio/goquery v1.10.0
github.com/WulfgarW/sensonet v0.0.2-0.20241226193935-3414de4a54fe
github.com/andig/go-powerwall v0.2.1-0.20230808194509-dd70cdb6e140
github.com/andig/gosunspec v0.0.0-20240918203654-860ce51d602b
github.com/andig/mbserver v0.0.0-20230310211055-1d29cbb5820e
@ -94,13 +95,13 @@ require (
github.com/writeas/go-strip-markdown/v2 v2.1.1
gitlab.com/bboehmke/sunny v0.16.0
go.uber.org/mock v0.5.0
golang.org/x/crypto v0.29.0
golang.org/x/crypto v0.31.0
golang.org/x/crypto/x509roots/fallback v0.0.0-20241127184453-8c4e668694cc
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f
golang.org/x/net v0.31.0
golang.org/x/oauth2 v0.24.0
golang.org/x/sync v0.9.0
golang.org/x/text v0.20.0
golang.org/x/sync v0.10.0
golang.org/x/text v0.21.0
golang.org/x/tools v0.27.0
google.golang.org/grpc v1.68.0
google.golang.org/protobuf v1.35.2
@ -191,8 +192,8 @@ require (
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/term v0.26.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/term v0.27.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
gopkg.in/go-playground/validator.v9 v9.31.0 // indirect
gopkg.in/sourcemap.v1 v1.0.5 // indirect

22
go.sum
View file

@ -23,6 +23,8 @@ github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWX
github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/WulfgarW/sensonet v0.0.2-0.20241226193935-3414de4a54fe h1:71CVWUNP3zeiJQQY7qYUzfUsY2DKiO5MVcEBe06b7fU=
github.com/WulfgarW/sensonet v0.0.2-0.20241226193935-3414de4a54fe/go.mod h1:QZodMch+UskApzlhhGa5ydSY0v5knlF4uQ5WO78xrEs=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/ahmetb/go-linq/v3 v3.2.0 h1:BEuMfp+b59io8g5wYzNoFe9pWPalRklhlhbiU3hYZDE=
github.com/ahmetb/go-linq/v3 v3.2.0/go.mod h1:haQ3JfOeWK8HpVxMtHHEMPVgBKiYyQ+f1/kLZh/cj9U=
@ -714,8 +716,8 @@ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto/x509roots/fallback v0.0.0-20241127184453-8c4e668694cc h1:N2DXFnxni8U4KZ7CcK6kicRat3uowReUjOJxwYTxQv8=
golang.org/x/crypto/x509roots/fallback v0.0.0-20241127184453-8c4e668694cc/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -777,8 +779,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -820,14 +822,14 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@ -836,8 +838,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View file

@ -0,0 +1,28 @@
template: vaillant
products:
- brand: Vaillant
description:
generic: SensoNET (SG Ready)
group: heating
requirements:
# evcc: ["sponsorship"]
description:
de: Die Boost Funktion erwärmt Warmwasser und optional eine Boostzone. Die Boostzone wird durch die ID identifiziert. Die Boost Temperatur wird in Grad Celsius angegeben.
en: The boost function heats hot water and optionally a boost zone. The boost zone is identified by the ID. The boost temperature is specified in degrees Celsius.
params:
- name: user
- name: password
- name: zone
type: int
help:
de: Id der Boostzone (zusätzlich zu Warmwasser)
en: Boost zone id (in addition to hot water)
- name: setpoint
type: float
help:
de: Boost Temperatur (zusätzlich zu Warmwasser)
en: Boost temperature setpoint (in addition to hot water)
render: |
type: vaillant
user: {{ .user }}
password: {{ .password }}