Add Viessmann (#20111)

This commit is contained in:
Michael Pollmeier 2025-05-29 15:57:01 +02:00 • committed by GitHub
parent d5c349757a
commit 09b3d7f077
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 274 additions and 1 deletions

View file

@ -1,6 +1,6 @@
<template>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-html="compiledMarkdown"></div>
<div class="root" v-html="compiledMarkdown"></div>
</template>
<script>
@ -18,3 +18,13 @@ export default {
},
};
</script>
<style scoped>
.root {
max-width: 100%;
}
.root :deep(pre.code) {
overflow-x: auto;
margin: 1em 0;
hyphens: none;
}
</style>

115
plugin/auth/viessmann.go Normal file
View file

@ -0,0 +1,115 @@
package auth
import (
"context"
"fmt"
"net/http"
"net/http/cookiejar"
"net/url"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
"github.com/evcc-io/evcc/util/transport"
"github.com/samber/lo"
"golang.org/x/oauth2"
)
const (
OAuthURI = "https://iam.viessmann.com/idp/v3"
RedirectURI = "http://localhost:4200/"
// ^ the value of RedirectURI doesn't matter, but it must be the same between requests
)
func OAuth2Config(clientID string) *oauth2.Config {
return &oauth2.Config{
ClientID: clientID,
Endpoint: oauth2.Endpoint{
AuthURL: OAuthURI + "/authorize",
TokenURL: OAuthURI + "/token",
AuthStyle: oauth2.AuthStyleInHeader,
},
RedirectURL: RedirectURI,
Scopes: []string{"IoT User", "offline_access"},
}
}
func init() {
registry.AddCtx("viessmann", NewViessmannFromConfig)
}
type Viessmann struct {
client *http.Client
ts oauth2.TokenSource
}
func NewViessmannFromConfig(ctx context.Context, other map[string]any) (Authorizer, error) {
var cc struct {
ClientID string
User, Password string
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
v := &Viessmann{
client: request.NewClient(util.NewLogger("viessmann")),
}
oc := OAuth2Config(cc.ClientID)
ctx = context.WithValue(context.Background(), oauth2.HTTPClient, v.client)
token, err := v.login(ctx, oc, cc.User, cc.Password)
if err != nil {
return nil, err
}
v.ts = oc.TokenSource(ctx, token)
return v, nil
}
func (v *Viessmann) Transport(base http.RoundTripper) (http.RoundTripper, error) {
return &oauth2.Transport{
Base: base,
Source: v.ts,
}, nil
}
func (v *Viessmann) login(ctx context.Context, oc *oauth2.Config, user, password string) (*oauth2.Token, error) {
cv := oauth2.GenerateVerifier()
state := lo.RandomString(16, lo.AlphanumericCharset)
uri := oc.AuthCodeURL(state, oauth2.S256ChallengeOption(cv))
v.client.Jar, _ = cookiejar.New(nil)
v.client.CheckRedirect = request.DontFollow
defer func() {
v.client.Jar = nil
v.client.CheckRedirect = nil
}()
req, _ := request.New(http.MethodGet, uri, nil, map[string]string{
"Authorization": transport.BasicAuthHeader(user, password),
})
resp, err := v.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusFound {
return nil, fmt.Errorf("unexpected status %d", resp.StatusCode)
}
loc, err := url.Parse(resp.Header.Get("Location"))
if err != nil {
return nil, err
}
code := loc.Query().Get("code")
ctx, cancel := context.WithTimeout(ctx, request.Timeout)
defer cancel()
return oc.Exchange(ctx, code, oauth2.VerifierOption(cv))
}

View file

@ -0,0 +1,148 @@
template: viessmann
products:
- brand: Viessmann
description:
generic: Heatpump (SG Ready)
group: heating
requirements:
# evcc: ["sponsorship"]
evcc: ["skiptest"]
description:
de: |
Einmalige Warmwasserbereitung auf eine konfigurierbare Solltemperatur. Das Gerät entscheidet eigenständig, ob die Wärmepumpe oder die elektrische Zusatzheizung (falls vorhanden) genutzt wird.
en: |
One-time hot water preparation to a configurable target temperature. The device automatically decides whether to use the heat pump or the auxiliary electric heater (if available).
params:
- name: user
required: true
help:
de: Registrieren auf [app.developer.viessmann.com](https://app.developer.viessmann.com)
en: Register at [app.developer.viessmann.com](https://app.developer.viessmann.com)
- name: password
required: true
help:
de: Für den konfigurierten Viessmann Account.
en: For the configured Viessmann account.
- name: clientid
required: true
description:
de: Client ID
en: Client ID
help:
de: Konfigurieren in [app.developer.viessmann.com](https://app.developer.viessmann.com)
en: Configure at [app.developer.viessmann.com](https://app.developer.viessmann.com)
- name: gateway_serial
required: true
description:
de: Gateway Serial
en: Gateway Serial
help:
de: Seriennummer des VitoConnect modul (VitoCare App -> Einstellungen -> Kommunikationsmodul -> Seriennummer)
en: VitoConnect serial number (VitoCare App -> Settings -> Communication module -> Serial number)
- name: installation_id
required: true
description:
de: Installation ID
en: Installation ID
help:
de: siehe https://docs.evcc.io/docs/devices/heating#viessmann
en: |
Unfortunately you cannot simply lookup this number in the Viessmann app - instead you need to use the following commands on the command line... we're aware this is not for every user, but currently we don't have a better workflow...<br/>
Prerequisites: curl, jq, and the following parameters:
```
VIESSMANN_USER=<your-user>
VIESSMANN_PASS=<your-password>
VIESSMANN_CLIENT_ID=<your-clientid>
```
Then execute the following (n.b. it's best to paste the entire block as-is, since the intermediate 'CODE' is only valid for 20 seconds):
```
VIESSMANN_REDIRECT_URI="http://localhost:4200/"
VIESSMANN_CODE_CHALLENGE="5M5nhkBfkWZCGfLZYcTL-l7esjPUN7PpZ4rq8k4cmys"
VIESSMANN_CODE_VERIFIER="6PygdmeK8JKPuuftlkc6q4ceyvjhMM_a_cJrPbcmcLc-SPjx2ZXTYr-SOofPUBydQ3McNYRy7Hibc2L2WtVLJFpOQ~Qbgic455ArKjUz9_UiTLnO6q8A3e.I_fIF8hAo"
```
- name: device_id
required: true
description:
de: Device ID
en: Device ID
help:
de: normalerweise `0`
en: typically `0`
default: 0
- name: target_temperature
description:
de: Zieltemperatur für Einmal-Warmwasser-Zubereitung (°C)
en: Target temperature for one-time charge (°C)
help:
de: max. 60°C
en: max. 60°C
required: true
default: 45
type: int
render: |
type: sgready
getmode:
source: http
uri: https://api.viessmann.com/iot/v2/features/installations/{{.installation_id}}/gateways/{{.gateway_serial}}/devices/{{.device_id}}/features/heating.dhw.oneTimeCharge
cache: 2s # to prevent making two identical requests straight after each other for "getmode"
auth:
source: viessmann
user: {{ .user }}
password: {{ .password }}
clientid: {{ .clientid }}
jq: '.data.properties.active.value | if . == false then 1 elif . == true then 2 else . end'
# false -> oneTimeCharge is disabled -> normal mode -> 1
# true -> oneTimeCharge is enabled -> boost mode -> 2
setmode:
source: watchdog
timeout: 60m # re-write at timeout/2
reset: 1 # reset watchdog on normal
set:
source: switch
switch:
- case: 1 # normal
set:
source: http
uri: https://api.viessmann.com/iot/v2/features/installations/{{.installation_id}}/gateways/{{.gateway_serial}}/devices/{{.device_id}}/features/heating.dhw.oneTimeCharge/commands/deactivate
method: POST
headers:
- content-type: application/json
auth:
source: viessmann
user: {{ .user }}
password: {{ .password }}
clientid: {{ .clientid }}
body: >
{ }
- case: 2 # boost
set:
source: sequence
set:
- source: http
uri: https://api.viessmann.com/iot/v2/features/installations/{{.installation_id}}/gateways/{{.gateway_serial}}/devices/{{.device_id}}/features/heating.dhw.temperature.temp2/commands/setTargetTemperature
method: POST
headers:
- content-type: application/json
auth:
source: viessmann
user: {{ .user }}
password: {{ .password }}
clientid: {{ .clientid }}
body: >
{"temperature": {{.target_temperature}}}
- source: http
uri: https://api.viessmann.com/iot/v2/features/installations/{{.installation_id}}/gateways/{{.gateway_serial}}/devices/{{.device_id}}/features/heating.dhw.oneTimeCharge/commands/activate
method: POST
headers:
- content-type: application/json
auth:
source: viessmann
user: {{ .user }}
password: {{ .password }}
clientid: {{ .clientid }}
body: >
{ }