mirror of
https://github.com/Xevion/go-ha.git
synced 2025-12-05 23:15:07 -06:00
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:
62
app.go
62
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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module example
|
||||
|
||||
go 1.21
|
||||
go 1.23
|
||||
|
||||
require (
|
||||
github.com/golang-cz/devslog v0.0.8
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user