diff --git a/charger/vaillant.go b/charger/vaillant.go new file mode 100644 index 000000000..500010839 --- /dev/null +++ b/charger/vaillant.go @@ -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) +} diff --git a/charger/vaillant_decorators.go b/charger/vaillant_decorators.go new file mode 100644 index 000000000..64d11601d --- /dev/null +++ b/charger/vaillant_decorators.go @@ -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() +} diff --git a/go.mod b/go.mod index e20aff375..6d4137a63 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 1115bbbb0..fa9fb8ef3 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/templates/definition/charger/vaillant.yaml b/templates/definition/charger/vaillant.yaml new file mode 100644 index 000000000..a859dd197 --- /dev/null +++ b/templates/definition/charger/vaillant.yaml @@ -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 }}