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.
This commit is contained in:
Matthias Loibl
2025-01-17 01:45:42 +01:00
parent 066441762b
commit b5d35235f8
7 changed files with 67 additions and 71 deletions

60
app.go
View File

@@ -5,20 +5,19 @@ import (
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
"net/url"
"time" "time"
"github.com/golang-module/carbon" "github.com/golang-module/carbon"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
sunriseLib "github.com/nathan-osman/go-sunrise" sunriseLib "github.com/nathan-osman/go-sunrise"
"saml.dev/gome-assistant/internal" "saml.dev/gome-assistant/internal"
"saml.dev/gome-assistant/internal/http" "saml.dev/gome-assistant/internal/http"
pq "saml.dev/gome-assistant/internal/priorityqueue" pq "saml.dev/gome-assistant/internal/priorityqueue"
ws "saml.dev/gome-assistant/internal/websocket" ws "saml.dev/gome-assistant/internal/websocket"
) )
// Returned by NewApp() if authentication fails
var ErrInvalidToken = ws.ErrInvalidToken
var ErrInvalidArgs = errors.New("invalid arguments provided") var ErrInvalidArgs = errors.New("invalid arguments provided")
type App struct { type App struct {
@@ -41,15 +40,11 @@ type App struct {
eventListeners map[string][]*EventListener eventListeners map[string][]*EventListener
} }
/* // DurationString represents a duration, such as "2s" or "24h".
DurationString represents a duration, such as "2s" or "24h". // See https://pkg.go.dev/time#ParseDuration for all valid time units.
See https://pkg.go.dev/time#ParseDuration for all valid time units.
*/
type DurationString string 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 TimeString string
type timeRange struct { type timeRange struct {
@@ -59,11 +54,16 @@ type timeRange struct {
type NewAppRequest struct { type NewAppRequest struct {
// Required // Required
URL string
// Optional
// Deprecated: use URL instead
// IpAddress of your Home Assistant instance i.e. "localhost" // IpAddress of your Home Assistant instance i.e. "localhost"
// or "192.168.86.59" etc. // or "192.168.86.59" etc.
IpAddress string IpAddress string
// Optional // Optional
// Deprecated: use URL instead
// Port number Home Assistant is running on. Defaults to 8123. // Port number Home Assistant is running on. Defaults to 8123.
Port string Port string
@@ -90,39 +90,37 @@ NewApp establishes the websocket connection and returns an object
you can use to register schedules and listeners. you can use to register schedules and listeners.
*/ */
func NewApp(request NewAppRequest) (*App, error) { func NewApp(request NewAppRequest) (*App, error) {
if request.IpAddress == "" || request.HAAuthToken == "" || request.HomeZoneEntityId == "" { if (request.URL == "" && request.IpAddress == "") || request.HAAuthToken == "" || request.HomeZoneEntityId == "" {
slog.Error("IpAddress, HAAuthToken, and HomeZoneEntityId are all required arguments in NewAppRequest") slog.Error("URL, HAAuthToken, and HomeZoneEntityId are all required arguments in NewAppRequest")
return nil, ErrInvalidArgs return nil, ErrInvalidArgs
} }
baseURL := &url.URL{}
if request.URL != "" {
var err error
baseURL, err = url.Parse(request.URL)
if err != nil {
return nil, ErrInvalidArgs
}
} else {
// This is deprecated and will be removed in a future release
port := request.Port port := request.Port
if port == "" { if port == "" {
port = "8123" port = "8123"
} }
baseURL.Host = request.IpAddress + ":" + port
var (
conn *websocket.Conn
ctx context.Context
ctxCancel context.CancelFunc
err error
)
if request.Secure {
conn, ctx, ctxCancel, err = ws.SetupSecureConnection(request.IpAddress, port, request.HAAuthToken)
} else {
conn, ctx, ctxCancel, err = ws.SetupConnection(request.IpAddress, port, request.HAAuthToken)
} }
conn, ctx, ctxCancel, err := ws.ConnectionFromUri(baseURL, request.HAAuthToken)
if err != nil {
return nil, err
}
if conn == nil { if conn == nil {
return nil, err return nil, err
} }
var httpClient *http.HttpClient httpClient := http.NewHttpClient(baseURL, request.HAAuthToken)
if request.Secure {
httpClient = http.NewHttpsClient(request.IpAddress, port, request.HAAuthToken)
} else {
httpClient = http.NewHttpClient(request.IpAddress, port, request.HAAuthToken)
}
wsWriter := &ws.WebsocketWriter{Conn: conn} wsWriter := &ws.WebsocketWriter{Conn: conn}
service := newService(wsWriter, ctx, httpClient) service := newService(wsWriter, ctx, httpClient)

View File

@@ -1,4 +1,4 @@
package example package main
import ( import (
"encoding/json" "encoding/json"
@@ -11,12 +11,12 @@ import (
func main() { func main() {
app, err := ga.NewApp(ga.NewAppRequest{ 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"), HAAuthToken: os.Getenv("HA_AUTH_TOKEN"),
HomeZoneEntityId: "zone.home", HomeZoneEntityId: "zone.home",
}) })
if err != nil { if err != nil {
slog.Error("Error connecting to HASS:", err) slog.Error("Error connecting to HASS:", "error", err)
os.Exit(1) os.Exit(1)
} }

View File

@@ -1,4 +1,4 @@
package example package main
import ( import (
"log/slog" "log/slog"
@@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
ga "saml.dev/gome-assistant" ga "saml.dev/gome-assistant"
) )
@@ -52,13 +53,13 @@ func (s *MySuite) SetupSuite() {
configFile, err := os.ReadFile("./config.yaml") configFile, err := os.ReadFile("./config.yaml")
if err != nil { if err != nil {
slog.Error("Error reading config file", err) slog.Error("Error reading config file", "error", err)
} }
s.config = &Config{} s.config = &Config{}
// either env var or config file can be used to set HA auth. token // either env var or config file can be used to set HA auth. token
s.config.Hass.HAAuthToken = os.Getenv("HA_AUTH_TOKEN") s.config.Hass.HAAuthToken = os.Getenv("HA_AUTH_TOKEN")
if err := yaml.Unmarshal(configFile, s.config); err != nil { 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{ s.app, err = ga.NewApp(ga.NewAppRequest{
@@ -67,7 +68,7 @@ func (s *MySuite) SetupSuite() {
HomeZoneEntityId: s.config.Hass.HomeZoneEntityId, HomeZoneEntityId: s.config.Hass.HomeZoneEntityId,
}) })
if err != nil { if err != nil {
slog.Error("Failed to createw new app", err) slog.Error("Failed to create new app", "error", err)
s.T().FailNow() s.T().FailNow()
} }
@@ -135,7 +136,7 @@ func (s *MySuite) dailyScheduleCallback(se *ga.Service, st ga.State) {
func getEntityState(s *MySuite, entityId string) string { func getEntityState(s *MySuite, entityId string) string {
state, err := s.app.GetState().Get(entityId) state, err := s.app.GetState().Get(entityId)
if err != nil { if err != nil {
slog.Error("Error getting entity state", err) slog.Error("Error getting entity state", "error", err)
s.T().FailNow() s.T().FailNow()
} }
slog.Info("State of entity", "state", state.State) slog.Info("State of entity", "state", state.State)

View File

@@ -1,6 +1,6 @@
module example module example
go 1.21 go 1.23
require ( require (
github.com/golang-cz/devslog v0.0.8 github.com/golang-cz/devslog v0.0.8

View File

@@ -5,9 +5,9 @@ package http
import ( import (
"errors" "errors"
"fmt"
"io" "io"
"net/http" "net/http"
"net/url"
) )
type HttpClient struct { type HttpClient struct {
@@ -15,24 +15,19 @@ type HttpClient struct {
token string token string
} }
func NewHttpClient(ip, port, token string) *HttpClient { func NewHttpClient(url *url.URL, token string) *HttpClient {
return ClientFromUri( // Shallow copy the URL to avoid modifying the original
fmt.Sprintf("http://%s:%s/api", ip, port), u := *url
token, 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{ return &HttpClient{
uri, url: u.String(),
token, token: token,
} }
} }

View File

@@ -10,10 +10,12 @@ import (
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
"net/url"
"sync" "sync"
"time" "time"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
i "saml.dev/gome-assistant/internal" i "saml.dev/gome-assistant/internal"
) )
@@ -49,25 +51,24 @@ func ReadMessage(conn *websocket.Conn, ctx context.Context) ([]byte, error) {
return msg, nil return msg, nil
} }
func SetupConnection(ip, port, authToken string) (*websocket.Conn, context.Context, context.CancelFunc, error) { func ConnectionFromUri(baseURL *url.URL, 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) {
ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*3) 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 // Init websocket connection
dialer := websocket.DefaultDialer dialer := websocket.DefaultDialer
conn, _, err := dialer.DialContext(ctx, uri, nil) conn, _, err := dialer.DialContext(ctx, urlWebsockets.String(), nil)
if err != nil { if err != nil {
ctxCancel() 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 return nil, nil, nil, err
} }

View File

@@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/golang-module/carbon" "github.com/golang-module/carbon"
"saml.dev/gome-assistant/internal/http" "saml.dev/gome-assistant/internal/http"
) )