Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 41 additions & 5 deletions util/homeassistant/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,42 @@ import (
"fmt"
"sync"

"github.com/evcc-io/evcc/plugin/auth"
"github.com/evcc-io/evcc/util"
"golang.org/x/oauth2"
)

func init() {
auth.Register("homeassistant", NewHomeAssistantFromConfig)
}

func NewHomeAssistantFromConfig(other map[string]any) (oauth2.TokenSource, error) {
var cc struct {
URI string
Home string // TODO remove deprecated
Insecure bool
}

if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}

uri := cc.URI

if uri == "" && cc.Home != "" {
uri = instanceUriByName(cc.Home)
if uri == "" {
return nil, fmt.Errorf("unknown instance: %s", cc.Home)
}
}

if ts, ok := supervisorTokenSource(uri); ok {
return ts, nil
}

return NewOAuth(uri, cc.Insecure)
}

type proxyInstance struct {
mu sync.Mutex
home, uri string
Expand Down Expand Up @@ -43,12 +76,15 @@ func (inst *proxyInstance) Token() (*oauth2.Token, error) {
defer inst.mu.Unlock()

if inst.TokenSource == nil {
ts, err := NewHomeAssistant(uri, inst.insecure)
if err != nil {
return nil, err
if ts, ok := supervisorTokenSource(uri); ok {
inst.TokenSource = ts
} else {
ts, err := NewOAuth(uri, inst.insecure)
if err != nil {
return nil, err
}
inst.TokenSource = ts
}

inst.TokenSource = ts
}

return inst.TokenSource.Token()
Expand Down
30 changes: 1 addition & 29 deletions util/homeassistant/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package homeassistant

import (
"context"
"fmt"
"net"
"net/http"
"net/url"
Expand All @@ -17,34 +16,7 @@ import (

// https://developers.home-assistant.io/docs/auth_api

func init() {
auth.Register("homeassistant", NewHomeAssistantFromConfig)
}

func NewHomeAssistantFromConfig(other map[string]any) (oauth2.TokenSource, error) {
var cc struct {
URI string
Home string // TODO remove deprecated
Insecure bool
}

if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}

uri := cc.URI

if uri == "" && cc.Home != "" {
uri = instanceUriByName(cc.Home)
if uri == "" {
return nil, fmt.Errorf("unknown instance: %s", cc.Home)
}
}

return NewHomeAssistant(uri, cc.Insecure)
}

func NewHomeAssistant(uri string, insecure bool) (oauth2.TokenSource, error) {
func NewOAuth(uri string, insecure bool) (oauth2.TokenSource, error) {
uri = strings.TrimRight(uri, "/") // normalize

extUrl := network.Config().ExternalURL()
Expand Down
31 changes: 31 additions & 0 deletions util/homeassistant/supervisor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package homeassistant

import (
"os"
"strings"

"golang.org/x/oauth2"
)

const (
SupervisorURI = "http://supervisor/core"
SupervisorToken = "SUPERVISOR_TOKEN"
SupervisorInstance = "HomeAssistant via EVCC App"
Comment thread
wlcrs marked this conversation as resolved.
Outdated
)

func init() {
if hasSupervisorToken() {
addInstance(SupervisorInstance, SupervisorURI)
}
}

func hasSupervisorToken() bool {
return os.Getenv(SupervisorToken) != ""
}

func supervisorTokenSource(uri string) (oauth2.TokenSource, bool) {
if token := os.Getenv(SupervisorToken); token != "" && strings.TrimRight(uri, "/") == SupervisorURI {
return oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}), true
}
return nil, false
}
73 changes: 73 additions & 0 deletions util/homeassistant/supervisor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package homeassistant

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/evcc-io/evcc/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestSupervisorToken(t *testing.T) {
t.Setenv(SupervisorToken, "test_supervisor_token")

ts, ok := supervisorTokenSource(SupervisorURI)
require.True(t, ok)

tok, err := ts.Token()
require.NoError(t, err)
assert.Equal(t, "test_supervisor_token", tok.AccessToken)
Comment thread
wlcrs marked this conversation as resolved.

// empty uri does not match supervisor
_, ok = supervisorTokenSource("")
assert.False(t, ok)

// other uri does not match supervisor
_, ok = supervisorTokenSource("http://homeassistant.local:8123")
assert.False(t, ok)

// from config with SupervisorURI
ts3, err := NewHomeAssistantFromConfig(map[string]any{"uri": SupervisorURI})
require.NoError(t, err)

tok3, err := ts3.Token()
require.NoError(t, err)
assert.Equal(t, "test_supervisor_token", tok3.AccessToken)
Comment thread
wlcrs marked this conversation as resolved.

// connection requires uri
_, err = NewConnection(util.NewLogger("test"), "", "", false)
assert.Error(t, err)

conn, err := NewConnection(util.NewLogger("test"), SupervisorURI, "", false)
require.NoError(t, err)
assert.Equal(t, SupervisorURI, conn.URI())

// test authenticated request using connection
var authHeader string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader = r.Header.Get("Authorization")
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`[{"entity_id":"sensor.test","state":"10"}]`))
}))
defer srv.Close()

testConn, err := NewConnection(util.NewLogger("test"), srv.URL, "", false)
require.NoError(t, err)
// override instance token source directly or test via proxyInstance
testConn.instance.TokenSource = ts

states, err := testConn.GetStates()
require.NoError(t, err)
assert.Len(t, states, 1)
assert.Equal(t, "Bearer test_supervisor_token", authHeader)
}

func TestSupervisorDiscovery(t *testing.T) {
t.Setenv(SupervisorToken, "test_supervisor_token")

addInstance(SupervisorInstance, SupervisorURI)
assert.Equal(t, SupervisorURI, instanceUriByName(SupervisorInstance))
assert.Equal(t, SupervisorInstance, instanceNameByUri(SupervisorURI))
}
Loading