diff --git a/ecobee/auth.go b/ecobee/auth.go index f5da038..31f7b48 100644 --- a/ecobee/auth.go +++ b/ecobee/auth.go @@ -17,6 +17,7 @@ package ecobee import ( "context" "encoding/json" + "errors" "fmt" "io/ioutil" "net/http" @@ -31,17 +32,45 @@ import ( var Scopes = []string{"smartRead", "smartWrite"} type tokenSource struct { - token oauth2.Token - cacheFile, clientID string + token oauth2.Token + clientID string + authCache AuthCache +} + +// AuthCache encapsulates persistent cache for OAuth2 token. +type AuthCache interface { + LoadTokenData() ([]byte, error) + SaveTokenData(data []byte) error +} + +type fileAuthCache struct { + fileName string +} + +func (p fileAuthCache) LoadTokenData() ([]byte, error) { + return ioutil.ReadFile(p.fileName) +} + +func (p fileAuthCache) SaveTokenData(data []byte) error { + return ioutil.WriteFile(p.fileName, data, 0777) +} + +// TokenCacheFile returns AuthCache that persists the OAuth2 token in the specified file. +func TokenCacheFile(fileName string) AuthCache { + return fileAuthCache{fileName} } func TokenSource(clientID, cacheFile string) oauth2.TokenSource { - return oauth2.ReuseTokenSource(nil, newTokenSource(clientID, cacheFile)) + return TokenSourceWithAuthCache(clientID, fileAuthCache{cacheFile}) } -func newTokenSource(clientID, cacheFile string) *tokenSource { - file, err := ioutil.ReadFile(cacheFile) - ets := tokenSource{clientID: clientID, cacheFile: cacheFile} +func TokenSourceWithAuthCache(clientID string, authCache AuthCache) oauth2.TokenSource { + return oauth2.ReuseTokenSource(nil, newTokenSource(clientID, authCache)) +} + +func newTokenSource(clientID string, authCache AuthCache) *tokenSource { + file, err := authCache.LoadTokenData() + ets := tokenSource{clientID: clientID, authCache: authCache} if err != nil { // no file, corrupted, or other problem: just start with an // empty token. @@ -56,8 +85,7 @@ func (ts *tokenSource) save() error { if err != nil { return err } - err = ioutil.WriteFile(ts.cacheFile, d, 0777) - return err + return ts.authCache.SaveTokenData(d) } func (ts *tokenSource) firstAuth() error { @@ -204,8 +232,38 @@ type Client struct { // (Application Key). Use the Ecobee Developer Portal to create the // Application Key. // (https://www.ecobee.com/consumerportal/index.html#/dev) +// This function is deprecated - use New() instead. func NewClient(clientID, authCache string) *Client { - return &Client{oauth2.NewClient( - context.Background(), - TokenSource(clientID, authCache))} + c, err := New(context.TODO(), clientID, Options{ + AuthCache: fileAuthCache{authCache}, + }) + + if err != nil { + // can't fail here. + panic("unexpected usage: " + err.Error()) + } + + return c +} + +// Options specifies EcoBee client parameters. +type Options struct { + ApplicationID string // application ID created in Ecobee Developer Portal + AuthCache AuthCache // token cache, typically usually TokenCacheFile(fileName) +} + +// New creates a Ecobee API client for the specific options +// Use the Ecobee Developer Portal to create the Application Key. +// (https://www.ecobee.com/consumerportal/index.html#/dev) +func New(ctx context.Context, clientID string, opt Options) (*Client, error) { + if opt.ApplicationID == "" { + return nil, errors.New("application ID is required") + } + if opt.AuthCache == nil { + return nil, fmt.Errorf("auth cache is required") + } + + ts := TokenSourceWithAuthCache(clientID, opt.AuthCache) + cli := oauth2.NewClient(ctx, ts) + return &Client{cli}, nil }