Skip to content

Commit

Permalink
Add README and docs
Browse files Browse the repository at this point in the history
  • Loading branch information
probakowski committed Nov 19, 2021
1 parent c5226ad commit 4af5e5d
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 27 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/go.yml → .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Go
name: Build

on:
push:
Expand Down
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# go-viessmann

go-viessmann is a Go client library for accessing the [Viessmann Cloud API](https://developer.viessmann.com/)

[![Build](https://github.com/probakowski/go-viessmann/actions/workflows/build.yml/badge.svg)](https://github.com/probakowski/go-viessmann/actions/workflows/build.yml)
[![Go Report Card](https://goreportcard.com/badge/github.com/probakowski/go-viessmann)](https://goreportcard.com/report/github.com/probakowski/go-viessmann)

## Installation

go-viessmann is compatible with modern Go releases in module mode, with Go installed:

```bash
go get github.com/probakowski/go-viessmann
```

will resolve and add the package to the current development module, along with its dependencies.

Alternatively the same can be achieved if you use import in a package:

```go
import "github.com/probakowski/go-viessmann"
```

and run `go get` without parameters.

Finally, to use the top-of-trunk version of this repo, use the following command:

```bash
go get github.com/probakowski/go-viessmann@master
```

## Usage ##

```go
import "github.com/probakowski/go-viessmann"
```

Construct a new viessmann client, then you can use different method from [API](https://developer.viessmann.com/), for
example:

```go
client := viessmann.Client{
ClientId: "<your_client_id"
RefreshToken "<OAuth refresh token"
HttpClient: client, //optional, HTTP client to use, http.DefaultClient will be used if nil
}

...
```

### Authentication

You have to register as developer on [Viessmann Developer Portal](https://developer.viessmann.com) and create
[API key](https://developer.viessmann.com/en/clients)

`viessmann.Client` uses OAuth refresh token to obtain access token that is required for API access. This token can be
obtained manually as described in [Viessmann documentation](https://developer.viessmann.com/en/doc/authentication)
or you can use server located in `example/server/go`:

1. Start server: `go run example/server.go`
2. Go to http://localhost:3000
3. Provide Client ID and click `Log in`, you'll be redirected to Viessmann login page when you have to log in with
credentials you used during registration
4. If everything is OK you will see overview of all your installations, gateways, devices and their features
5. Refresh token will be stored in `config` file in current directory
35 changes: 18 additions & 17 deletions example/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"github.com/probakowski/go-viessmann"
"html/template"
"io"
"io/fs"
"io/ioutil"
"log"
Expand All @@ -19,14 +20,8 @@ import (
//go:embed static
var web embed.FS

type Config struct {
RefreshToken string `json:"refresh_token"`
ClientId string `json:"client_id"`
AccessToken string `json:"access_token"`
}

func main() {
config := &viessmann.Api{}
config := &viessmann.Client{}
mu := sync.Mutex{}
configBytes, err := ioutil.ReadFile("config")
if err == nil {
Expand Down Expand Up @@ -55,13 +50,13 @@ func main() {
if token == "" {
http.Redirect(w, req, "/login.html", http.StatusSeeOther)
} else {
buf := bytes.NewBufferString("")
buf := &bytes.Buffer{}
err = tmpl.Execute(buf, config)
if err != nil {
http.Error(w, err.Error(), 500)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_, _ = w.Write(buf.Bytes())
_, _ = io.Copy(w, buf)
}
} else {
fileServer.ServeHTTP(w, req)
Expand Down Expand Up @@ -105,17 +100,23 @@ func main() {
http.Error(w, string(body), res.StatusCode)
return
}
auth := Config{}
err = json.Unmarshal(body, &auth)
mu.Lock()
defer mu.Unlock()
err = json.Unmarshal(body, &config)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
configBytes, err := json.Marshal(config)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = ioutil.WriteFile("config", configBytes, 0600)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
mu.Lock()
config.RefreshToken = auth.RefreshToken
configBytes, _ := json.Marshal(config)
mu.Unlock()
_ = ioutil.WriteFile("config", configBytes, 0600)
http.Redirect(w, req, "/", http.StatusFound)
}
})
Expand Down
41 changes: 32 additions & 9 deletions viessmann.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,34 @@ import (

const base = "https://api.viessmann.com/iot/v1/equipment/"

type Api struct {
// Client for Viessmann API
type Client struct {
ClientId string `json:"client_id"`
RefreshToken string `json:"refresh_token"`
accessToken string
valid time.Time
mu sync.Mutex
HttpClient HttpClient `json:"-"`
}

// HttpClient to use for requests
type HttpClient interface {
Do(req *http.Request) (*http.Response, error)
}

type token struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
}

// Location of installation
type Location struct {
Latitude float64
Longitude float64
TimeZone string
}

// Address of installation
type Address struct {
Street string
HouseNumber string
Expand All @@ -41,8 +50,10 @@ type Address struct {
Location Location `json:"geolocation"`
}

// Installation - When connecting your heating system, you are registering a new installation.
// The installations contain your gateway and the device, which is the heating system itself.
type Installation struct {
api *Api
api *Client
Id int
Description string
Address Address
Expand All @@ -60,8 +71,9 @@ type installationListWrapper struct {
Data []Installation
}

// Gateway - (aka. wi-fi module) device that connects an HVAC installation to the cloud.
type Gateway struct {
api *Api
api *Client
Serial string
Version string
FailedFirmwareUpdates int `json:"firmwareUpdateFailureCounter"`
Expand All @@ -84,8 +96,10 @@ type gatewayListWrapper struct {
Data []Gateway
}

// Device - Device that is a part of the installation. A device is for example the heating system itself or
// room control elements.
type Device struct {
api *Api
api *Client
Id string
BoilerSerial string
BoilerSerialEditor string
Expand All @@ -103,21 +117,24 @@ type deviceListWrapper struct {
Data []Device
}

// Parameter that can be passed to Command
type Parameter struct {
Type string
Required bool
Constraints map[string]interface{}
}

// Command that can be invoked to change Feature values
type Command struct {
Name string
Uri string
Executable bool `json:"isExecutable"`
Params map[string]Parameter
}

// Feature - Object representing some part of gateway/device state. The feature contains commands.
type Feature struct {
api *Api
api *Client
Name string `json:"feature"`
Uri string
Properties map[string]map[string]interface{}
Expand All @@ -131,7 +148,8 @@ type featureListWrapper struct {
Data []Feature
}

func (v *Api) Installation(id string) (Installation, error) {
// Installation returns installation by its id
func (v *Client) Installation(id string) (Installation, error) {
var i installationWrapper
err := v.get(fmt.Sprintf("installations/%s", id), &i)
if err != nil {
Expand All @@ -141,7 +159,8 @@ func (v *Api) Installation(id string) (Installation, error) {
return i.Data, nil
}

func (v *Api) Installations() ([]Installation, error) {
// Installations returns all installations
func (v *Client) Installations() ([]Installation, error) {
var i installationListWrapper
err := v.get("installations", &i)
if err != nil {
Expand All @@ -153,6 +172,7 @@ func (v *Api) Installations() ([]Installation, error) {
return i.Data, nil
}

// Gateway returns gateway associated with this Installation by its serial
func (i Installation) Gateway(serial string) (Gateway, error) {
var g gatewayWrapper
err := i.api.get(fmt.Sprintf("installations/%d/%s", i.Id, serial), &g)
Expand All @@ -163,6 +183,7 @@ func (i Installation) Gateway(serial string) (Gateway, error) {
return g.Data, nil
}

// Gateways returns all gateways associated with this Installation
func (i Installation) Gateways() ([]Gateway, error) {
var g gatewayListWrapper
err := i.api.get(fmt.Sprintf("installations/%d/gateways", i.Id), &g)
Expand All @@ -175,6 +196,7 @@ func (i Installation) Gateways() ([]Gateway, error) {
return g.Data, nil
}

// Devices returns all devices associated with this Gateway
func (g Gateway) Devices() ([]Device, error) {
var d deviceListWrapper
err := g.api.get(fmt.Sprintf("installations/%d/gateways/%s/devices", g.InstallationId, g.Serial), &d)
Expand All @@ -189,6 +211,7 @@ func (g Gateway) Devices() ([]Device, error) {
return d.Data, nil
}

// Features returns all features associated with this Device
func (d Device) Features() ([]Feature, error) {
var f featureListWrapper
err := d.api.get(fmt.Sprintf("installations/%d/gateways/%s/devices/%s/features", d.InstallationId, d.GatewaySerial, d.Id), &f)
Expand All @@ -198,7 +221,7 @@ func (d Device) Features() ([]Feature, error) {
return f.Data, nil
}

func (v *Api) refreshAccessToken() error {
func (v *Client) refreshAccessToken() error {
v.mu.Lock()
defer v.mu.Unlock()
if v.valid.After(time.Now().Add(10 * time.Minute)) {
Expand Down Expand Up @@ -231,7 +254,7 @@ func (v *Api) refreshAccessToken() error {
return nil
}

func (v *Api) get(path string, t interface{}) error {
func (v *Client) get(path string, t interface{}) error {
err := v.refreshAccessToken()
if err != nil {
return err
Expand Down

0 comments on commit 4af5e5d

Please sign in to comment.