Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
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
47 changes: 42 additions & 5 deletions util/homeassistant/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,43 @@ 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)
}

// NewHomeAssistantFromConfig creates a Home Assistant token source from configuration
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 +77,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
31 changes: 2 additions & 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,8 @@ 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) {
// NewOAuth creates a Home Assistant OAuth token source
func NewOAuth(uri string, insecure bool) (oauth2.TokenSource, error) {
uri = strings.TrimRight(uri, "/") // normalize

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

import (
"os"
"strings"

"golang.org/x/oauth2"
)

const (
// SupervisorURI is the Home Assistant Core API endpoint when running as a Home Assistant add-on
SupervisorURI = "http://supervisor/core"
// SupervisorToken is the environment variable name containing the bearer token
SupervisorToken = "SUPERVISOR_TOKEN"
// SupervisorInstance is the discovered instance name for the Supervisor integration
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
}
91 changes: 91 additions & 0 deletions util/homeassistant/supervisor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
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.

// SupervisorURI variant with trailing slash should still match
tsSlash, ok := supervisorTokenSource(SupervisorURI + "/")
require.True(t, ok)

tokSlash, err := tsSlash.Token()
require.NoError(t, err)
assert.Equal(t, "test_supervisor_token", tokSlash.AccessToken)

// 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.

// when SUPERVISOR_TOKEN is unset, NewHomeAssistantFromConfig should fall back to the standard OAuth token source
t.Setenv(SupervisorToken, "")

ts4, err := NewHomeAssistantFromConfig(map[string]any{"uri": SupervisorURI})
require.NoError(t, err)

_, err = ts4.Token()
require.Error(t, err)
assert.ErrorContains(t, err, "login required")
Comment thread
wlcrs marked this conversation as resolved.
Outdated

// 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