mirror of
https://github.com/Xevion/go-ha.git
synced 2025-12-05 23:15:07 -06:00
refactor: websockets into 'connect' module, rename & adjust generally
This commit is contained in:
30
app.go
30
app.go
@@ -14,8 +14,8 @@ import (
|
|||||||
sunriseLib "github.com/nathan-osman/go-sunrise"
|
sunriseLib "github.com/nathan-osman/go-sunrise"
|
||||||
|
|
||||||
"github.com/Workiva/go-datastructures/queue"
|
"github.com/Workiva/go-datastructures/queue"
|
||||||
internal "github.com/Xevion/go-ha/internal"
|
"github.com/Xevion/go-ha/internal"
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
"github.com/Xevion/go-ha/internal/connect"
|
||||||
"github.com/Xevion/go-ha/types"
|
"github.com/Xevion/go-ha/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,10 +24,9 @@ var ErrInvalidArgs = errors.New("invalid arguments provided")
|
|||||||
type App struct {
|
type App struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
ctxCancel context.CancelFunc
|
ctxCancel context.CancelFunc
|
||||||
conn *websocket.Conn
|
|
||||||
|
|
||||||
// Wraps the ws connection with added mutex locking
|
// Wraps the ws connection with added mutex locking
|
||||||
wsWriter *ws.WebsocketWriter
|
conn *connect.HAConnection
|
||||||
|
|
||||||
httpClient *internal.HttpClient
|
httpClient *internal.HttpClient
|
||||||
|
|
||||||
@@ -101,18 +100,14 @@ func NewApp(request types.NewAppRequest) (*App, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, ctx, ctxCancel, err := ws.ConnectionFromUri(baseURL, request.HAAuthToken)
|
conn, ctx, ctxCancel, err := connect.ConnectionFromUri(baseURL, request.HAAuthToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if conn == nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
httpClient := internal.NewHttpClient(baseURL, request.HAAuthToken)
|
httpClient := internal.NewHttpClient(ctx, baseURL, request.HAAuthToken)
|
||||||
|
|
||||||
wsWriter := &ws.WebsocketWriter{Conn: conn}
|
service := newService(conn)
|
||||||
service := newService(wsWriter)
|
|
||||||
state, err := newState(httpClient, request.HomeZoneEntityId)
|
state, err := newState(httpClient, request.HomeZoneEntityId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -125,7 +120,6 @@ func NewApp(request types.NewAppRequest) (*App, error) {
|
|||||||
|
|
||||||
return &App{
|
return &App{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
wsWriter: wsWriter,
|
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
ctxCancel: ctxCancel,
|
ctxCancel: ctxCancel,
|
||||||
httpClient: httpClient,
|
httpClient: httpClient,
|
||||||
@@ -151,14 +145,14 @@ func (app *App) Close() error {
|
|||||||
// Close websocket connection if it exists
|
// Close websocket connection if it exists
|
||||||
if app.conn != nil {
|
if app.conn != nil {
|
||||||
deadline := time.Now().Add(10 * time.Second)
|
deadline := time.Now().Add(10 * time.Second)
|
||||||
err := app.conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), deadline)
|
err := app.conn.Conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), deadline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("Error writing close message", "error", err)
|
slog.Warn("Error writing close message", "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the websocket connection
|
// Close the websocket connection
|
||||||
err = app.conn.Close()
|
err = app.conn.Conn.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("Error closing websocket connection", "error", err)
|
slog.Warn("Error closing websocket connection", "error", err)
|
||||||
return err
|
return err
|
||||||
@@ -249,7 +243,7 @@ func (app *App) RegisterEventListeners(evls ...EventListener) {
|
|||||||
if elList, ok := app.eventListeners[eventType]; ok {
|
if elList, ok := app.eventListeners[eventType]; ok {
|
||||||
app.eventListeners[eventType] = append(elList, &evl)
|
app.eventListeners[eventType] = append(elList, &evl)
|
||||||
} else {
|
} else {
|
||||||
ws.SubscribeToEventType(eventType, app.wsWriter, app.ctx)
|
connect.SubscribeToEventType(eventType, app.conn, app.ctx)
|
||||||
app.eventListeners[eventType] = []*EventListener{&evl}
|
app.eventListeners[eventType] = []*EventListener{&evl}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -309,7 +303,7 @@ func (app *App) Start() {
|
|||||||
|
|
||||||
// subscribe to state_changed events
|
// subscribe to state_changed events
|
||||||
id := internal.NextId()
|
id := internal.NextId()
|
||||||
ws.SubscribeToStateChangedEvents(id, app.wsWriter, app.ctx)
|
connect.SubscribeToStateChangedEvents(id, app.conn, app.ctx)
|
||||||
app.entityListenersId = id
|
app.entityListenersId = id
|
||||||
|
|
||||||
// Run entity listeners startup
|
// Run entity listeners startup
|
||||||
@@ -337,8 +331,8 @@ func (app *App) Start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// entity listeners and event listeners
|
// entity listeners and event listeners
|
||||||
elChan := make(chan ws.ChanMsg, 100) // Add buffer to prevent channel overflow
|
elChan := make(chan connect.ChannelMessage, 100) // Add buffer to prevent channel overflow
|
||||||
go ws.ListenWebsocket(app.conn, elChan)
|
go connect.ListenWebsocket(app.conn.Conn, elChan)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"github.com/golang-module/carbon"
|
"github.com/golang-module/carbon"
|
||||||
|
|
||||||
"github.com/Xevion/go-ha/internal"
|
"github.com/Xevion/go-ha/internal"
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
ws "github.com/Xevion/go-ha/internal/connect"
|
||||||
"github.com/Xevion/go-ha/types"
|
"github.com/Xevion/go-ha/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@ type BaseEventMsg struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Functions */
|
/* Functions */
|
||||||
func callEventListeners(app *App, msg ws.ChanMsg) {
|
func callEventListeners(app *App, msg ws.ChannelMessage) {
|
||||||
baseEventMsg := BaseEventMsg{}
|
baseEventMsg := BaseEventMsg{}
|
||||||
_ = json.Unmarshal(msg.Raw, &baseEventMsg)
|
_ = json.Unmarshal(msg.Raw, &baseEventMsg)
|
||||||
listeners, ok := app.eventListeners[baseEventMsg.Event.EventType]
|
listeners, ok := app.eventListeners[baseEventMsg.Event.EventType]
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// websocket API. All HA interaction is done via websocket
|
// websocket API. All HA interaction is done via websocket
|
||||||
// except for cases explicitly called out in http package
|
// except for cases explicitly called out in http package
|
||||||
// documentation.
|
// documentation.
|
||||||
package websocket
|
package connect
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -20,46 +20,60 @@ import (
|
|||||||
|
|
||||||
var ErrInvalidToken = errors.New("invalid authentication token")
|
var ErrInvalidToken = errors.New("invalid authentication token")
|
||||||
|
|
||||||
type AuthMessage struct {
|
// HAConnection is a wrapper around a websocket connection that provides a mutex for thread safety.
|
||||||
MsgType string `json:"type"`
|
type HAConnection struct {
|
||||||
AccessToken string `json:"access_token"`
|
Conn *websocket.Conn // Note: this is not thread safe except for Close() and WriteControl()
|
||||||
}
|
|
||||||
|
|
||||||
type WebsocketWriter struct {
|
|
||||||
Conn *websocket.Conn
|
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WebsocketWriter) WriteMessage(msg any) error {
|
// WriteMessage writes a message to the websocket connection.
|
||||||
|
func (w *HAConnection) WriteMessage(msg any) error {
|
||||||
w.mutex.Lock()
|
w.mutex.Lock()
|
||||||
defer w.mutex.Unlock()
|
defer w.mutex.Unlock()
|
||||||
|
|
||||||
return w.Conn.WriteJSON(msg)
|
return w.Conn.WriteJSON(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReadMessage(conn *websocket.Conn) ([]byte, error) {
|
// ReadMessageRaw reads a raw message from the websocket connection.
|
||||||
|
func ReadMessageRaw(conn *websocket.Conn) ([]byte, error) {
|
||||||
_, msg, err := conn.ReadMessage()
|
_, msg, err := conn.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []byte{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return msg, nil
|
return msg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConnectionFromUri(baseURL *url.URL, authToken string) (*websocket.Conn, context.Context, context.CancelFunc, error) {
|
// ReadMessage reads a message from the websocket connection and unmarshals it into the given type.
|
||||||
|
func ReadMessage[T any](conn *websocket.Conn) (T, error) {
|
||||||
|
var result T
|
||||||
|
_, msg, err := conn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(msg, &result)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectionFromUri creates a new websocket connection from the given base URL and authentication token.
|
||||||
|
func ConnectionFromUri(baseUrl *url.URL, token string) (*HAConnection, context.Context, context.CancelFunc, error) {
|
||||||
|
// Build the websocket URL
|
||||||
|
urlWebsockets := *baseUrl
|
||||||
|
urlWebsockets.Path = "/api/websocket"
|
||||||
|
scheme, err := internal.GetEquivalentWebsocketScheme(baseUrl.Scheme)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, fmt.Errorf("failed to build websocket URL: %w", err)
|
||||||
|
}
|
||||||
|
urlWebsockets.Scheme = scheme
|
||||||
|
|
||||||
// Create a short timeout context for the connection only
|
// Create a short timeout context for the connection only
|
||||||
connCtx, connCtxCancel := context.WithTimeout(context.Background(), time.Second*3)
|
connCtx, connCtxCancel := context.WithTimeout(context.Background(), time.Second*3)
|
||||||
defer connCtxCancel() // Always cancel the connection context when we're done
|
defer connCtxCancel() // Always cancel the connection context when we're done
|
||||||
|
|
||||||
// Shallow copy the URL to avoid modifying the original
|
|
||||||
urlWebsockets := *baseURL
|
|
||||||
urlWebsockets.Path = "/api/websocket"
|
|
||||||
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(connCtx, urlWebsockets.String(), nil)
|
conn, _, err := dialer.DialContext(connCtx, urlWebsockets.String(), nil)
|
||||||
@@ -69,14 +83,19 @@ func ConnectionFromUri(baseURL *url.URL, authToken string) (*websocket.Conn, con
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read auth_required message
|
// Read auth_required message
|
||||||
_, err = ReadMessage(conn)
|
msg, err := ReadMessage[struct {
|
||||||
|
MsgType string `json:"type"`
|
||||||
|
}](conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Unknown error creating websocket client\n")
|
slog.Error("Unknown error creating websocket client\n")
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
|
} else if msg.MsgType != "auth_required" {
|
||||||
|
slog.Error("Expected auth_required message, got", "msgType", msg.MsgType)
|
||||||
|
return nil, nil, nil, fmt.Errorf("expected auth_required message, got %s", msg.MsgType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send auth message
|
// Send auth message
|
||||||
err = SendAuthMessage(conn, connCtx, authToken)
|
err = SendAuthMessage(conn, connCtx, token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Unknown error creating websocket client\n")
|
slog.Error("Unknown error creating websocket client\n")
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
@@ -92,51 +111,54 @@ func ConnectionFromUri(baseURL *url.URL, authToken string) (*websocket.Conn, con
|
|||||||
// Create a new background context for the application lifecycle (no timeout)
|
// Create a new background context for the application lifecycle (no timeout)
|
||||||
appCtx, appCtxCancel := context.WithCancel(context.Background())
|
appCtx, appCtxCancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
return conn, appCtx, appCtxCancel, nil
|
return &HAConnection{Conn: conn}, appCtx, appCtxCancel, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendAuthMessage sends an auth message to the websocket connection.
|
||||||
func SendAuthMessage(conn *websocket.Conn, ctx context.Context, token string) error {
|
func SendAuthMessage(conn *websocket.Conn, ctx context.Context, token string) error {
|
||||||
|
type AuthMessage struct {
|
||||||
|
MsgType string `json:"type"`
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
}
|
||||||
|
|
||||||
err := conn.WriteJSON(AuthMessage{MsgType: "auth", AccessToken: token})
|
err := conn.WriteJSON(AuthMessage{MsgType: "auth", AccessToken: token})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type authResponse struct {
|
// VerifyAuthResponse verifies that the auth response is valid.
|
||||||
MsgType string `json:"type"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func VerifyAuthResponse(conn *websocket.Conn, ctx context.Context) error {
|
func VerifyAuthResponse(conn *websocket.Conn, ctx context.Context) error {
|
||||||
msg, err := ReadMessage(conn)
|
msg, err := ReadMessage[struct {
|
||||||
|
MsgType string `json:"type"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}](conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var authResp authResponse
|
if msg.MsgType != "auth_ok" {
|
||||||
err = json.Unmarshal(msg, &authResp)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if authResp.MsgType != "auth_ok" {
|
|
||||||
return ErrInvalidToken
|
return ErrInvalidToken
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubEvent struct {
|
func SubscribeToStateChangedEvents(id int64, conn *HAConnection, ctx context.Context) {
|
||||||
Id int64 `json:"id"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
EventType string `json:"event_type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func SubscribeToStateChangedEvents(id int64, conn *WebsocketWriter, ctx context.Context) {
|
|
||||||
SubscribeToEventType("state_changed", conn, ctx, id)
|
SubscribeToEventType("state_changed", conn, ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SubscribeToEventType(eventType string, conn *WebsocketWriter, ctx context.Context, id ...int64) {
|
// TODO: Instead of using variadic arguments, just use a nillable pointer for the id
|
||||||
|
func SubscribeToEventType(eventType string, conn *HAConnection, ctx context.Context, id ...int64) {
|
||||||
|
type SubEvent struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
EventType string `json:"event_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no id is provided, generate a new one
|
||||||
var finalId int64
|
var finalId int64
|
||||||
if len(id) == 0 {
|
if len(id) == 0 {
|
||||||
finalId = internal.NextId()
|
finalId = internal.NextId()
|
||||||
@@ -148,12 +170,12 @@ func SubscribeToEventType(eventType string, conn *WebsocketWriter, ctx context.C
|
|||||||
Type: "subscribe_events",
|
Type: "subscribe_events",
|
||||||
EventType: eventType,
|
EventType: eventType,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := conn.WriteMessage(e)
|
err := conn.WriteMessage(e)
|
||||||
|
// TODO: Handle errors better
|
||||||
if err != nil {
|
if err != nil {
|
||||||
wrappedErr := fmt.Errorf("error writing to websocket: %w", err)
|
wrappedErr := fmt.Errorf("error writing to websocket: %w", err)
|
||||||
slog.Error(wrappedErr.Error())
|
slog.Error(wrappedErr.Error())
|
||||||
panic(wrappedErr)
|
panic(wrappedErr)
|
||||||
}
|
}
|
||||||
// m, _ := ReadMessage(conn, ctx)
|
|
||||||
// log.Default().Println(string(m))
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package websocket
|
package connect
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -7,22 +7,26 @@ import (
|
|||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// BaseMessage is the base message type for all messages sent by the websocket server.
|
||||||
type BaseMessage struct {
|
type BaseMessage struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"` // not present in all messages
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChanMsg struct {
|
type ChannelMessage struct {
|
||||||
Id int64
|
Id int64
|
||||||
Type string
|
Type string
|
||||||
Success bool
|
Success bool
|
||||||
Raw []byte
|
Raw []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListenWebsocket(conn *websocket.Conn, c chan ChanMsg) {
|
// ListenWebsocket reads messages from the websocket connection and sends them to the channel.
|
||||||
|
// It will close the channel if it encounters an error, or if the channel is full, and return.
|
||||||
|
// It ignores errors in deserialization.
|
||||||
|
func ListenWebsocket(conn *websocket.Conn, c chan ChannelMessage) {
|
||||||
for {
|
for {
|
||||||
bytes, err := ReadMessage(conn)
|
raw, err := ReadMessageRaw(conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Error reading from websocket", "err", err)
|
slog.Error("Error reading from websocket", "err", err)
|
||||||
close(c)
|
close(c)
|
||||||
@@ -33,20 +37,26 @@ func ListenWebsocket(conn *websocket.Conn, c chan ChanMsg) {
|
|||||||
// default to true for messages that don't include "success" at all
|
// default to true for messages that don't include "success" at all
|
||||||
Success: true,
|
Success: true,
|
||||||
}
|
}
|
||||||
_ = json.Unmarshal(bytes, &base)
|
err = json.Unmarshal(raw, &base)
|
||||||
if !base.Success {
|
if err != nil {
|
||||||
slog.Warn("Received unsuccessful response", "response", string(bytes))
|
slog.Error("Error unmarshalling message", "err", err, "message", string(raw))
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
chanMsg := ChanMsg{
|
if !base.Success {
|
||||||
|
slog.Warn("Received unsuccessful response", "response", string(raw))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a channel message from the raw message
|
||||||
|
channelMessage := ChannelMessage{
|
||||||
Type: base.Type,
|
Type: base.Type,
|
||||||
Id: base.Id,
|
Id: base.Id,
|
||||||
Success: base.Success,
|
Success: base.Success,
|
||||||
Raw: bytes,
|
Raw: raw,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use non-blocking send to avoid hanging on closed channel
|
// Use non-blocking send to avoid hanging on closed channel
|
||||||
select {
|
select {
|
||||||
case c <- chanMsg:
|
case c <- channelMessage:
|
||||||
// Message sent successfully
|
// Message sent successfully
|
||||||
default:
|
default:
|
||||||
// Channel is full or closed, break out of loop
|
// Channel is full or closed, break out of loop
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
package internal
|
package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
@@ -12,40 +13,44 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type HttpClient struct {
|
type HttpClient struct {
|
||||||
client *resty.Client
|
client *resty.Client
|
||||||
|
baseRequest *resty.Request
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHttpClient(url *url.URL, token string) *HttpClient {
|
func NewHttpClient(ctx context.Context, baseUrl *url.URL, token string) *HttpClient {
|
||||||
// Shallow copy the URL to avoid modifying the original
|
// Shallow copy the URL to avoid modifying the original
|
||||||
u := *url
|
u := *baseUrl
|
||||||
u.Path = "/api"
|
u.Path = "/api"
|
||||||
if u.Scheme == "ws" {
|
|
||||||
u.Scheme = "http"
|
|
||||||
}
|
|
||||||
if u.Scheme == "wss" {
|
|
||||||
u.Scheme = "https"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create resty client with configuration
|
// Create resty client with configuration
|
||||||
client := resty.New().
|
client := resty.New().
|
||||||
SetBaseURL(u.String()).
|
SetBaseURL(u.String()).
|
||||||
SetHeader("Authorization", "Bearer "+token).
|
SetTimeout(30*time.Second).
|
||||||
SetTimeout(30 * time.Second).
|
|
||||||
SetRetryCount(3).
|
SetRetryCount(3).
|
||||||
SetRetryWaitTime(1 * time.Second).
|
SetRetryWaitTime(1*time.Second).
|
||||||
SetRetryMaxWaitTime(5 * time.Second).
|
SetRetryMaxWaitTime(5*time.Second).
|
||||||
AddRetryConditions(func(r *resty.Response, err error) bool {
|
AddRetryConditions(func(r *resty.Response, err error) bool {
|
||||||
return err != nil || r.StatusCode() >= 500
|
return err != nil || (r.StatusCode() >= 500 && r.StatusCode() != 403)
|
||||||
})
|
}).
|
||||||
|
SetHeader("User-Agent", "go-ha/"+currentVersion).
|
||||||
|
SetContext(ctx)
|
||||||
|
|
||||||
return &HttpClient{
|
return &HttpClient{
|
||||||
client: client,
|
client: client,
|
||||||
|
baseRequest: client.R().
|
||||||
|
SetContentType("application/json").
|
||||||
|
SetHeader("Accept", "application/json").
|
||||||
|
SetAuthToken(token),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getRequest returns a new request
|
||||||
|
func (c *HttpClient) getRequest() *resty.Request {
|
||||||
|
return c.baseRequest.Clone(c.client.Context())
|
||||||
|
}
|
||||||
|
|
||||||
func (c *HttpClient) GetState(entityId string) ([]byte, error) {
|
func (c *HttpClient) GetState(entityId string) ([]byte, error) {
|
||||||
resp, err := c.client.R().
|
resp, err := c.getRequest().Get("/states/" + entityId)
|
||||||
Get("/states/" + entityId)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("Error making HTTP request: " + err.Error())
|
return nil, errors.New("Error making HTTP request: " + err.Error())
|
||||||
@@ -58,9 +63,9 @@ func (c *HttpClient) GetState(entityId string) ([]byte, error) {
|
|||||||
return resp.Bytes(), nil
|
return resp.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *HttpClient) States() ([]byte, error) {
|
// GetStates returns the states of all entities.
|
||||||
resp, err := c.client.R().
|
func (c *HttpClient) GetStates() ([]byte, error) {
|
||||||
Get("/states")
|
resp, err := c.getRequest().Get("/states")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("Error making HTTP request: " + err.Error())
|
return nil, errors.New("Error making HTTP request: " + err.Error())
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package internal
|
package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -12,6 +13,10 @@ type EnabledDisabledInfo struct {
|
|||||||
RunOnError bool
|
RunOnError bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
currentVersion = "0.7.0"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
id atomic.Int64 // default value is 0
|
id atomic.Int64 // default value is 0
|
||||||
)
|
)
|
||||||
@@ -26,3 +31,20 @@ func NextId() int64 {
|
|||||||
func GetFunctionName(i interface{}) string {
|
func GetFunctionName(i interface{}) string {
|
||||||
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
|
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetEquivalentWebsocketScheme returns the equivalent websocket scheme for the given scheme.
|
||||||
|
// If the scheme is http or https, it returns ws or wss respectively.
|
||||||
|
// If the scheme is ws or wss, it returns the same scheme.
|
||||||
|
// If the scheme is not any of the above, it returns an error.
|
||||||
|
func GetEquivalentWebsocketScheme(scheme string) (string, error) {
|
||||||
|
switch scheme {
|
||||||
|
case "http":
|
||||||
|
return "ws", nil
|
||||||
|
case "https":
|
||||||
|
return "wss", nil
|
||||||
|
case "ws", "wss":
|
||||||
|
return scheme, nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unexpected scheme: %s", scheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
"github.com/Xevion/go-ha/internal/connect"
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Structs */
|
/* Structs */
|
||||||
|
|
||||||
type AdaptiveLighting struct {
|
type AdaptiveLighting struct {
|
||||||
conn *ws.WebsocketWriter
|
conn *connect.HAConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Public API */
|
/* Public API */
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
ws "github.com/Xevion/go-ha/internal/connect"
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Structs */
|
/* Structs */
|
||||||
|
|
||||||
type AlarmControlPanel struct {
|
type AlarmControlPanel struct {
|
||||||
conn *ws.WebsocketWriter
|
conn *ws.HAConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Public API */
|
/* Public API */
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
"github.com/Xevion/go-ha/internal/connect"
|
||||||
"github.com/Xevion/go-ha/types"
|
"github.com/Xevion/go-ha/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Structs */
|
/* Structs */
|
||||||
|
|
||||||
type Climate struct {
|
type Climate struct {
|
||||||
conn *ws.WebsocketWriter
|
conn *connect.HAConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Public API */
|
/* Public API */
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
"github.com/Xevion/go-ha/internal/connect"
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Structs */
|
/* Structs */
|
||||||
|
|
||||||
type Cover struct {
|
type Cover struct {
|
||||||
conn *ws.WebsocketWriter
|
conn *connect.HAConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Public API */
|
/* Public API */
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/Xevion/go-ha/internal"
|
"github.com/Xevion/go-ha/internal"
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
"github.com/Xevion/go-ha/internal/connect"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Event struct {
|
type Event struct {
|
||||||
conn *ws.WebsocketWriter
|
conn *connect.HAConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fire an event
|
// Fire an event
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
"github.com/Xevion/go-ha/internal/connect"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HomeAssistant struct {
|
type HomeAssistant struct {
|
||||||
conn *ws.WebsocketWriter
|
conn *connect.HAConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
// TurnOn a Home Assistant entity. Takes an entityId and an optional
|
// TurnOn a Home Assistant entity. Takes an entityId and an optional
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
ws "github.com/Xevion/go-ha/internal/connect"
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Structs */
|
/* Structs */
|
||||||
|
|
||||||
type InputBoolean struct {
|
type InputBoolean struct {
|
||||||
conn *ws.WebsocketWriter
|
conn *ws.HAConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Public API */
|
/* Public API */
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
ws "github.com/Xevion/go-ha/internal/connect"
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Structs */
|
/* Structs */
|
||||||
|
|
||||||
type InputButton struct {
|
type InputButton struct {
|
||||||
conn *ws.WebsocketWriter
|
conn *ws.HAConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Public API */
|
/* Public API */
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
ws "github.com/Xevion/go-ha/internal/connect"
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Structs */
|
/* Structs */
|
||||||
|
|
||||||
type InputDatetime struct {
|
type InputDatetime struct {
|
||||||
conn *ws.WebsocketWriter
|
conn *ws.HAConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Public API */
|
/* Public API */
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
ws "github.com/Xevion/go-ha/internal/connect"
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Structs */
|
/* Structs */
|
||||||
|
|
||||||
type InputNumber struct {
|
type InputNumber struct {
|
||||||
conn *ws.WebsocketWriter
|
conn *ws.HAConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Public API */
|
/* Public API */
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
"github.com/Xevion/go-ha/internal/connect"
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Structs */
|
/* Structs */
|
||||||
|
|
||||||
type InputText struct {
|
type InputText struct {
|
||||||
conn *ws.WebsocketWriter
|
conn *connect.HAConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Public API */
|
/* Public API */
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
ws "github.com/Xevion/go-ha/internal/connect"
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Structs */
|
/* Structs */
|
||||||
|
|
||||||
type Light struct {
|
type Light struct {
|
||||||
conn *ws.WebsocketWriter
|
conn *ws.HAConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Public API */
|
/* Public API */
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
ws "github.com/Xevion/go-ha/internal/connect"
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Structs */
|
/* Structs */
|
||||||
|
|
||||||
type Lock struct {
|
type Lock struct {
|
||||||
conn *ws.WebsocketWriter
|
conn *ws.HAConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Public API */
|
/* Public API */
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
ws "github.com/Xevion/go-ha/internal/connect"
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Structs */
|
/* Structs */
|
||||||
|
|
||||||
type MediaPlayer struct {
|
type MediaPlayer struct {
|
||||||
conn *ws.WebsocketWriter
|
conn *ws.HAConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Public API */
|
/* Public API */
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
ws "github.com/Xevion/go-ha/internal/connect"
|
||||||
"github.com/Xevion/go-ha/types"
|
"github.com/Xevion/go-ha/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Notify struct {
|
type Notify struct {
|
||||||
conn *ws.WebsocketWriter
|
conn *ws.HAConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify sends a notification. Takes a types.NotifyRequest.
|
// Notify sends a notification. Takes a types.NotifyRequest.
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
ws "github.com/Xevion/go-ha/internal/connect"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Number struct {
|
type Number struct {
|
||||||
conn *ws.WebsocketWriter
|
conn *ws.HAConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ib Number) SetValue(entityId string, value float32) error {
|
func (ib Number) SetValue(entityId string, value float32) error {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
ws "github.com/Xevion/go-ha/internal/connect"
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Structs */
|
/* Structs */
|
||||||
|
|
||||||
type Scene struct {
|
type Scene struct {
|
||||||
conn *ws.WebsocketWriter
|
conn *ws.HAConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Public API */
|
/* Public API */
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
ws "github.com/Xevion/go-ha/internal/connect"
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Structs */
|
/* Structs */
|
||||||
|
|
||||||
type Script struct {
|
type Script struct {
|
||||||
conn *ws.WebsocketWriter
|
conn *ws.HAConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Public API */
|
/* Public API */
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/Xevion/go-ha/internal"
|
"github.com/Xevion/go-ha/internal"
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
ws "github.com/Xevion/go-ha/internal/connect"
|
||||||
)
|
)
|
||||||
|
|
||||||
func BuildService[
|
func BuildService[
|
||||||
@@ -29,7 +29,7 @@ func BuildService[
|
|||||||
Timer |
|
Timer |
|
||||||
Vacuum |
|
Vacuum |
|
||||||
ZWaveJS,
|
ZWaveJS,
|
||||||
](conn *ws.WebsocketWriter) *T {
|
](conn *ws.HAConnection) *T {
|
||||||
return &T{conn: conn}
|
return &T{conn: conn}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
ws "github.com/Xevion/go-ha/internal/connect"
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Structs */
|
/* Structs */
|
||||||
|
|
||||||
type Switch struct {
|
type Switch struct {
|
||||||
conn *ws.WebsocketWriter
|
conn *ws.HAConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Public API */
|
/* Public API */
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
ws "github.com/Xevion/go-ha/internal/connect"
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Structs */
|
/* Structs */
|
||||||
|
|
||||||
type Timer struct {
|
type Timer struct {
|
||||||
conn *ws.WebsocketWriter
|
conn *ws.HAConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Public API */
|
/* Public API */
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
connect "github.com/Xevion/go-ha/internal/connect"
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Structs */
|
/* Structs */
|
||||||
|
|
||||||
type TTS struct {
|
type TTS struct {
|
||||||
conn *ws.WebsocketWriter
|
conn *connect.HAConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Public API */
|
/* Public API */
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
ws "github.com/Xevion/go-ha/internal/connect"
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Structs */
|
/* Structs */
|
||||||
|
|
||||||
type Vacuum struct {
|
type Vacuum struct {
|
||||||
conn *ws.WebsocketWriter
|
conn *ws.HAConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Public API */
|
/* Public API */
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
ws "github.com/Xevion/go-ha/internal/connect"
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Structs */
|
/* Structs */
|
||||||
|
|
||||||
type ZWaveJS struct {
|
type ZWaveJS struct {
|
||||||
conn *ws.WebsocketWriter
|
conn *ws.HAConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Public API */
|
/* Public API */
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package gomeassistant
|
package gomeassistant
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/Xevion/go-ha/internal/connect"
|
||||||
"github.com/Xevion/go-ha/internal/services"
|
"github.com/Xevion/go-ha/internal/services"
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
@@ -31,7 +31,7 @@ type Service struct {
|
|||||||
ZWaveJS *services.ZWaveJS
|
ZWaveJS *services.ZWaveJS
|
||||||
}
|
}
|
||||||
|
|
||||||
func newService(conn *ws.WebsocketWriter) *Service {
|
func newService(conn *connect.HAConnection) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
AdaptiveLighting: services.BuildService[services.AdaptiveLighting](conn),
|
AdaptiveLighting: services.BuildService[services.AdaptiveLighting](conn),
|
||||||
AlarmControlPanel: services.BuildService[services.AlarmControlPanel](conn),
|
AlarmControlPanel: services.BuildService[services.AlarmControlPanel](conn),
|
||||||
|
|||||||
2
state.go
2
state.go
@@ -83,7 +83,7 @@ func (s *StateImpl) Get(entityId string) (EntityState, error) {
|
|||||||
// ListEntities returns a list of all entities in Home Assistant.
|
// ListEntities returns a list of all entities in Home Assistant.
|
||||||
// see rest documentation for more details: https://developers.home-assistant.io/docs/api/rest/#actions
|
// see rest documentation for more details: https://developers.home-assistant.io/docs/api/rest/#actions
|
||||||
func (s *StateImpl) ListEntities() ([]EntityState, error) {
|
func (s *StateImpl) ListEntities() ([]EntityState, error) {
|
||||||
resp, err := s.httpClient.States()
|
resp, err := s.httpClient.GetStates()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user