Skip to content

Commit

Permalink
fix(hass): 🐛 rework marshaling of sensor requests
Browse files Browse the repository at this point in the history
- ensure different requests get marshaled into correct format
- handle unknown error from api
  • Loading branch information
joshuar committed Oct 26, 2024
1 parent 3de2c09 commit 1c579d9
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 72 deletions.
74 changes: 29 additions & 45 deletions internal/hass/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"github.com/go-resty/resty/v2"

"github.com/joshuar/go-hass-agent/internal/hass/sensor"
"github.com/joshuar/go-hass-agent/internal/logging"
)

Expand All @@ -35,14 +36,8 @@ func init() {
AddRetryCondition(defaultRetryFunc)
}

type RawRequest interface {
RequestBody() any
}

// Request is a HTTP POST request with the request body provided by Body().
type Request interface {
RequestType() string
RequestData() any
RequestBody() any
}

// Authenticated represents a request that requires passing an authentication
Expand All @@ -61,11 +56,6 @@ type Validator interface {
Validate() error
}

type requestBody struct {
Data any `json:"data"`
RequestType string `json:"type"`
}

type ResponseError struct {
Code any `json:"code,omitempty"`
Message string `json:"message,omitempty"`
Expand All @@ -89,14 +79,9 @@ func (e *ResponseError) Error() string {
}

func Send[T any](ctx context.Context, url string, details any) (T, error) {
var (
response T
responseErr ResponseError
responseObj *resty.Response
)
var response T

requestClient := client.R().SetContext(ctx)
requestClient = requestClient.SetError(&responseErr)
requestClient = requestClient.SetResult(&response)

// If the request is authenticated, set the auth header with the token.
Expand All @@ -111,31 +96,34 @@ func Send[T any](ctx context.Context, url string, details any) (T, error) {
}
}

// Explicitly set the request body based on the type. Not doing this with
// composable types may result in an unexpected body.
switch request := details.(type) {
case sensor.Entity:
requestClient.SetBody(request.RequestBody())
case sensor.State:
requestClient.SetBody(request.RequestBody())
case Request:
body := &requestBody{
RequestType: request.RequestType(),
Data: request.RequestData(),
}
logging.FromContext(ctx).
LogAttrs(ctx, logging.LevelTrace,
"Sending request.",
slog.String("method", "POST"),
slog.String("url", url),
slog.Any("body", body),
slog.Time("sent_at", time.Now()))

responseObj, _ = requestClient.SetBody(body).Post(url) //nolint:errcheck // error is checked with responseObj.IsError()
case RawRequest:
logging.FromContext(ctx).
LogAttrs(ctx, logging.LevelTrace,
"Sending request.",
slog.String("method", "POST"),
slog.String("url", url),
slog.Any("body", request),
slog.Time("sent_at", time.Now()))

responseObj, _ = requestClient.SetBody(request).Post(url) //nolint:errcheck // error is checked with responseObj.IsError()
requestClient.SetBody(request.RequestBody())
}

responseObj, err := requestClient.Post(url)

logging.FromContext(ctx).
LogAttrs(ctx, logging.LevelTrace,
"Sending request.",
slog.String("method", "POST"),
slog.String("url", url),
slog.Any("body", details),
slog.Time("sent_at", time.Now()))

switch {
case err != nil:
return response, fmt.Errorf("error sending request: %w", err)
case responseObj == nil:
return response, fmt.Errorf("unknown error sending request")
case responseObj.IsError():
return response, &ResponseError{Code: responseObj.StatusCode(), Message: responseObj.Status()}
}

logging.FromContext(ctx).
Expand All @@ -147,9 +135,5 @@ func Send[T any](ctx context.Context, url string, details any) (T, error) {
slog.Duration("time", responseObj.Time()),
slog.String("body", string(responseObj.Body())))

if responseObj.IsError() {
return response, &ResponseError{Code: responseObj.StatusCode(), Message: responseObj.Status()}
}

return response, nil
}
2 changes: 1 addition & 1 deletion internal/hass/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ func (c *Client) ProcessSensor(ctx context.Context, details sensor.Entity) error

resp, err := api.Send[bulkSensorUpdateResponse](ctx, c.url, details.State)
if err != nil {
return fmt.Errorf("failed to send sensor update: %w", err)
return fmt.Errorf("failed to send sensor update for %s: %w", details.Name, err)
}

go resp.Process(ctx, details)
Expand Down
4 changes: 0 additions & 4 deletions internal/hass/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@ const (

type responseStatus int

type Response interface {
Status() (responseStatus, error)
}

type response struct {
ErrorDetails *api.ResponseError `json:"error,omitempty"`
IsSuccess bool `json:"success,omitempty"`
Expand Down
46 changes: 24 additions & 22 deletions internal/hass/sensor/entities.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ const (
requestTypeLocation = "update_location"
)

type Request struct {
Data any `json:"data"`
RequestType string `json:"type"`
}

type State struct {
Value any `json:"state" validate:"required"`
Attributes map[string]any `json:"attributes,omitempty" validate:"omitempty"`
Expand All @@ -36,6 +41,13 @@ func (s *State) Validate() error {
return nil
}

func (s *State) RequestBody() any {
return &Request{
RequestType: requestTypeUpdateSensor,
Data: s,
}
}

//nolint:wrapcheck
func (s *State) MarshalJSON() ([]byte, error) {
return json.Marshal(&struct {
Expand All @@ -53,14 +65,6 @@ func (s *State) MarshalJSON() ([]byte, error) {
})
}

func (s *State) RequestType() string {
return requestTypeUpdateSensor
}

func (s *State) RequestData() any {
return s
}

type Entity struct {
*State
Name string `json:"name" validate:"required"`
Expand All @@ -79,6 +83,13 @@ func (e *Entity) Validate() error {
return nil
}

func (e *Entity) RequestBody() any {
return &Request{
RequestType: requestTypeRegisterSensor,
Data: e,
}
}

//nolint:wrapcheck
func (e *Entity) MarshalJSON() ([]byte, error) {
return json.Marshal(&struct {
Expand Down Expand Up @@ -106,14 +117,6 @@ func (e *Entity) MarshalJSON() ([]byte, error) {
})
}

func (e *Entity) RequestType() string {
return requestTypeRegisterSensor
}

func (e *Entity) RequestData() any {
return e
}

// Location represents the location information that can be sent to HA to
// update the location of the agent. This is exposed so that device code can
// create location requests directly, as Home Assistant handles these
Expand All @@ -137,10 +140,9 @@ func (l *Location) Validate() error {
return nil
}

func (l *Location) RequestType() string {
return requestTypeLocation
}

func (l *Location) RequestData() any {
return l
func (l *Location) RequestBody() any {
return &Request{
RequestType: requestTypeLocation,
Data: l,
}
}

0 comments on commit 1c579d9

Please sign in to comment.