diff --git a/assets/js/components/Config/Markdown.vue b/assets/js/components/Config/Markdown.vue
index ffe7da61d..424778e62 100644
--- a/assets/js/components/Config/Markdown.vue
+++ b/assets/js/components/Config/Markdown.vue
@@ -1,6 +1,6 @@
-
+
+
diff --git a/plugin/auth/viessmann.go b/plugin/auth/viessmann.go
new file mode 100644
index 000000000..116da2d8e
--- /dev/null
+++ b/plugin/auth/viessmann.go
@@ -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))
+}
diff --git a/templates/definition/charger/viessmann.yaml b/templates/definition/charger/viessmann.yaml
new file mode 100644
index 000000000..63bec3526
--- /dev/null
+++ b/templates/definition/charger/viessmann.yaml
@@ -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...
+
+ Prerequisites: curl, jq, and the following parameters:
+
+ ```
+ VIESSMANN_USER=
+ VIESSMANN_PASS=
+ VIESSMANN_CLIENT_ID=
+ ```
+
+ 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: >
+ { }