Home Assistant: auto-detect Supervisor instance and token (#33000)
This commit is contained in:
parent
e63bae0c8b
commit
6715711d49
4 changed files with 170 additions and 34 deletions
|
|
@ -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,13 +77,16 @@ func (inst *proxyInstance) Token() (*oauth2.Token, error) {
|
|||
defer inst.mu.Unlock()
|
||||
|
||||
if inst.TokenSource == nil {
|
||||
ts, err := NewHomeAssistant(uri, inst.insecure)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
return inst.TokenSource.Token()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
34
util/homeassistant/supervisor.go
Normal file
34
util/homeassistant/supervisor.go
Normal 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
|
||||
}
|
||||
92
util/homeassistant/supervisor_test.go
Normal file
92
util/homeassistant/supervisor_test.go
Normal 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))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue