From b5d35235f8c72594d6db7f831b9accb6fe81f1fa Mon Sep 17 00:00:00 2001 From: Matthias Loibl Date: Fri, 17 Jan 2025 01:45:42 +0100 Subject: [PATCH] Parse URL and pass it to clients Right now, this SDK only works with IP:Port. I'm however running on https://home.example.com and need https or wss and an implicit port 443. Using net/url should be the best option. --- app.go | 62 ++++++++++++++++----------------- example/example.go | 6 ++-- example/example_live_test.go | 11 +++--- example/go.mod | 2 +- internal/http/http.go | 29 +++++++-------- internal/websocket/websocket.go | 27 +++++++------- state.go | 1 + 7 files changed, 67 insertions(+), 71 deletions(-) diff --git a/app.go b/app.go index 4cd6e79..16ecb4d 100644 --- a/app.go +++ b/app.go @@ -5,20 +5,19 @@ import ( "errors" "fmt" "log/slog" + "net/url" "time" "github.com/golang-module/carbon" "github.com/gorilla/websocket" sunriseLib "github.com/nathan-osman/go-sunrise" + "saml.dev/gome-assistant/internal" "saml.dev/gome-assistant/internal/http" pq "saml.dev/gome-assistant/internal/priorityqueue" ws "saml.dev/gome-assistant/internal/websocket" ) -// Returned by NewApp() if authentication fails -var ErrInvalidToken = ws.ErrInvalidToken - var ErrInvalidArgs = errors.New("invalid arguments provided") type App struct { @@ -41,15 +40,11 @@ type App struct { eventListeners map[string][]*EventListener } -/* -DurationString represents a duration, such as "2s" or "24h". -See https://pkg.go.dev/time#ParseDuration for all valid time units. -*/ +// DurationString represents a duration, such as "2s" or "24h". +// See https://pkg.go.dev/time#ParseDuration for all valid time units. type DurationString string -/* -TimeString is a 24-hr format time "HH:MM" such as "07:30". -*/ +// TimeString is a 24-hr format time "HH:MM" such as "07:30". type TimeString string type timeRange struct { @@ -59,11 +54,16 @@ type timeRange struct { type NewAppRequest struct { // Required + URL string + + // Optional + // Deprecated: use URL instead // IpAddress of your Home Assistant instance i.e. "localhost" // or "192.168.86.59" etc. IpAddress string // Optional + // Deprecated: use URL instead // Port number Home Assistant is running on. Defaults to 8123. Port string @@ -90,39 +90,37 @@ NewApp establishes the websocket connection and returns an object you can use to register schedules and listeners. */ func NewApp(request NewAppRequest) (*App, error) { - if request.IpAddress == "" || request.HAAuthToken == "" || request.HomeZoneEntityId == "" { - slog.Error("IpAddress, HAAuthToken, and HomeZoneEntityId are all required arguments in NewAppRequest") + if (request.URL == "" && request.IpAddress == "") || request.HAAuthToken == "" || request.HomeZoneEntityId == "" { + slog.Error("URL, HAAuthToken, and HomeZoneEntityId are all required arguments in NewAppRequest") return nil, ErrInvalidArgs } - port := request.Port - if port == "" { - port = "8123" - } - var ( - conn *websocket.Conn - ctx context.Context - ctxCancel context.CancelFunc - err error - ) + baseURL := &url.URL{} - if request.Secure { - conn, ctx, ctxCancel, err = ws.SetupSecureConnection(request.IpAddress, port, request.HAAuthToken) + if request.URL != "" { + var err error + baseURL, err = url.Parse(request.URL) + if err != nil { + return nil, ErrInvalidArgs + } } else { - conn, ctx, ctxCancel, err = ws.SetupConnection(request.IpAddress, port, request.HAAuthToken) + // This is deprecated and will be removed in a future release + port := request.Port + if port == "" { + port = "8123" + } + baseURL.Host = request.IpAddress + ":" + port } + conn, ctx, ctxCancel, err := ws.ConnectionFromUri(baseURL, request.HAAuthToken) + if err != nil { + return nil, err + } if conn == nil { return nil, err } - var httpClient *http.HttpClient - - if request.Secure { - httpClient = http.NewHttpsClient(request.IpAddress, port, request.HAAuthToken) - } else { - httpClient = http.NewHttpClient(request.IpAddress, port, request.HAAuthToken) - } + httpClient := http.NewHttpClient(baseURL, request.HAAuthToken) wsWriter := &ws.WebsocketWriter{Conn: conn} service := newService(wsWriter, ctx, httpClient) diff --git a/example/example.go b/example/example.go index 6d77d46..f4e2f76 100644 --- a/example/example.go +++ b/example/example.go @@ -1,4 +1,4 @@ -package example +package main import ( "encoding/json" @@ -11,12 +11,12 @@ import ( func main() { app, err := ga.NewApp(ga.NewAppRequest{ - IpAddress: "192.168.86.67", // Replace with your Home Assistant IP Address + URL: "http://192.168.86.67:8123", // Replace with your Home Assistant IP Address HAAuthToken: os.Getenv("HA_AUTH_TOKEN"), HomeZoneEntityId: "zone.home", }) if err != nil { - slog.Error("Error connecting to HASS:", err) + slog.Error("Error connecting to HASS:", "error", err) os.Exit(1) } diff --git a/example/example_live_test.go b/example/example_live_test.go index e636a22..93a9623 100644 --- a/example/example_live_test.go +++ b/example/example_live_test.go @@ -1,4 +1,4 @@ -package example +package main import ( "log/slog" @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "gopkg.in/yaml.v3" + ga "saml.dev/gome-assistant" ) @@ -52,13 +53,13 @@ func (s *MySuite) SetupSuite() { configFile, err := os.ReadFile("./config.yaml") if err != nil { - slog.Error("Error reading config file", err) + slog.Error("Error reading config file", "error", err) } s.config = &Config{} // either env var or config file can be used to set HA auth. token s.config.Hass.HAAuthToken = os.Getenv("HA_AUTH_TOKEN") if err := yaml.Unmarshal(configFile, s.config); err != nil { - slog.Error("Error unmarshalling config file", err) + slog.Error("Error unmarshalling config file", "error", err) } s.app, err = ga.NewApp(ga.NewAppRequest{ @@ -67,7 +68,7 @@ func (s *MySuite) SetupSuite() { HomeZoneEntityId: s.config.Hass.HomeZoneEntityId, }) if err != nil { - slog.Error("Failed to createw new app", err) + slog.Error("Failed to create new app", "error", err) s.T().FailNow() } @@ -135,7 +136,7 @@ func (s *MySuite) dailyScheduleCallback(se *ga.Service, st ga.State) { func getEntityState(s *MySuite, entityId string) string { state, err := s.app.GetState().Get(entityId) if err != nil { - slog.Error("Error getting entity state", err) + slog.Error("Error getting entity state", "error", err) s.T().FailNow() } slog.Info("State of entity", "state", state.State) diff --git a/example/go.mod b/example/go.mod index 49184a0..a595e74 100644 --- a/example/go.mod +++ b/example/go.mod @@ -1,6 +1,6 @@ module example -go 1.21 +go 1.23 require ( github.com/golang-cz/devslog v0.0.8 diff --git a/internal/http/http.go b/internal/http/http.go index 2f89643..8fee706 100644 --- a/internal/http/http.go +++ b/internal/http/http.go @@ -5,9 +5,9 @@ package http import ( "errors" - "fmt" "io" "net/http" + "net/url" ) type HttpClient struct { @@ -15,24 +15,19 @@ type HttpClient struct { token string } -func NewHttpClient(ip, port, token string) *HttpClient { - return ClientFromUri( - fmt.Sprintf("http://%s:%s/api", ip, port), - token, - ) -} +func NewHttpClient(url *url.URL, token string) *HttpClient { + // Shallow copy the URL to avoid modifying the original + u := *url + if u.Scheme == "ws" { + u.Scheme = "http" + } + if u.Scheme == "wss" { + u.Scheme = "https" + } -func NewHttpsClient(ip, port, token string) *HttpClient { - return ClientFromUri( - fmt.Sprintf("https://%s:%s/api", ip, port), - token, - ) -} - -func ClientFromUri(uri, token string) *HttpClient { return &HttpClient{ - uri, - token, + url: u.String(), + token: token, } } diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go index 2eec28b..17b6725 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -10,10 +10,12 @@ import ( "errors" "fmt" "log/slog" + "net/url" "sync" "time" "github.com/gorilla/websocket" + i "saml.dev/gome-assistant/internal" ) @@ -49,25 +51,24 @@ func ReadMessage(conn *websocket.Conn, ctx context.Context) ([]byte, error) { return msg, nil } -func SetupConnection(ip, port, authToken string) (*websocket.Conn, context.Context, context.CancelFunc, error) { - uri := fmt.Sprintf("ws://%s:%s/api/websocket", ip, port) - return ConnectionFromUri(uri, authToken) -} - -func SetupSecureConnection(ip, port, authToken string) (*websocket.Conn, context.Context, context.CancelFunc, error) { - uri := fmt.Sprintf("wss://%s:%s/api/websocket", ip, port) - return ConnectionFromUri(uri, authToken) -} - -func ConnectionFromUri(uri, authToken string) (*websocket.Conn, context.Context, context.CancelFunc, error) { +func ConnectionFromUri(baseURL *url.URL, authToken string) (*websocket.Conn, context.Context, context.CancelFunc, error) { ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*3) + // Shallow copy the URL to avoid modifying the original + urlWebsockets := *baseURL + if baseURL.Scheme == "http" { + urlWebsockets.Scheme = "ws" + } + if baseURL.Scheme == "https" { + urlWebsockets.Scheme = "wss" + } + // Init websocket connection dialer := websocket.DefaultDialer - conn, _, err := dialer.DialContext(ctx, uri, nil) + conn, _, err := dialer.DialContext(ctx, urlWebsockets.String(), nil) if err != nil { ctxCancel() - slog.Error("Failed to connect to websocket. Check URI\n", "uri", uri) + slog.Error("Failed to connect to websocket. Check URI\n", "url", urlWebsockets) return nil, nil, nil, err } diff --git a/state.go b/state.go index edc9c91..0f365dd 100644 --- a/state.go +++ b/state.go @@ -7,6 +7,7 @@ import ( "time" "github.com/golang-module/carbon" + "saml.dev/gome-assistant/internal/http" )