Home Assistant: auto-detect Supervisor instance and token (#33000)

This commit is contained in:
Thijs W. 2026-08-20 13:37:50 +02:00 • committed by GitHub
parent e63bae0c8b
commit 6715711d49
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 170 additions and 34 deletions

View file

@ -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
@ -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()

View file

@ -2,7 +2,6 @@ package homeassistant
import (
"context"
"fmt"
"net"
"net/http"
"net/url"
@ -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()

View file

@ -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 Host"
)
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
}

View file

@ -0,0 +1,92 @@
package homeassistant
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/evcc-io/evcc/api"
"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)
// 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)
// 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)
var elr *api.ErrLoginRequired
assert.ErrorAs(t, err, &elr)
// 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))
}