diff --git a/internal/hass/client.go b/internal/hass/client.go index 85f77ea7..efb39657 100644 --- a/internal/hass/client.go +++ b/internal/hass/client.go @@ -99,7 +99,10 @@ func (c *Client) ProcessEvent(ctx context.Context, details event.Event) error { func (c *Client) ProcessSensor(ctx context.Context, details sensor.Entity) error { // Location request. if req, ok := details.Value.(*sensor.Location); ok { - resp, err := api.Send[response](ctx, c.url, req) + resp, err := api.Send[response](ctx, c.url, + sensor.NewRequest( + sensor.AsLocationUpdate(*req), + )) if err != nil { return fmt.Errorf("failed to send location update: %w", err) } @@ -122,7 +125,11 @@ func (c *Client) ProcessSensor(ctx context.Context, details sensor.Entity) error return nil } - resp, err := api.Send[bulkSensorUpdateResponse](ctx, c.url, details.State) + resp, err := api.Send[bulkSensorUpdateResponse](ctx, c.url, + sensor.NewRequest( + sensor.AsSensorUpdate(details), + sensor.AsRetryable(details.RetryRequest), + )) if err != nil { return fmt.Errorf("failed to send sensor update for %s: %w", details.Name, err) } @@ -133,7 +140,11 @@ func (c *Client) ProcessSensor(ctx context.Context, details sensor.Entity) error } // Sensor registration. - resp, err := api.Send[registrationResponse](ctx, c.url, &details) + resp, err := api.Send[registrationResponse](ctx, c.url, + sensor.NewRequest( + sensor.AsSensorRegistration(details), + sensor.AsRetryable(details.RetryRequest), + )) if err != nil { return fmt.Errorf("failed to send sensor registration: %w", err) } diff --git a/internal/hass/sensor/entities.go b/internal/hass/sensor/entities.go index 43241240..5de97d52 100644 --- a/internal/hass/sensor/entities.go +++ b/internal/hass/sensor/entities.go @@ -5,7 +5,6 @@ package sensor import ( - "encoding/json" "fmt" "maps" @@ -15,19 +14,10 @@ import ( const ( StateUnknown = "Unknown" - - requestTypeRegisterSensor = "register_sensor" - requestTypeUpdateSensor = "update_sensor_states" - requestTypeLocation = "update_location" ) type Option[T any] func(T) T -type Request struct { - Data any `json:"data"` - RequestType string `json:"type"` -} - type RequestMetadata struct { RetryRequest bool } @@ -43,101 +33,103 @@ type State struct { // WithValue assigns a value to the sensor. func WithValue(value any) Option[State] { - return func(s State) State { - s.Value = value - return s + return func(state State) State { + state.Value = value + return state } } // WithAttributes sets the additional attributes for the sensor. func WithAttributes(attributes map[string]any) Option[State] { - return func(s State) State { - if s.Attributes != nil { - maps.Copy(s.Attributes, attributes) + return func(state State) State { + if state.Attributes != nil { + maps.Copy(state.Attributes, attributes) } else { - s.Attributes = attributes + state.Attributes = attributes } - return s + + return state } } // WithAttribute sets the given additional attribute to the given value. func WithAttribute(name string, value any) Option[State] { - return func(s State) State { - if s.Attributes == nil { - s.Attributes = make(map[string]any) + return func(state State) State { + if state.Attributes == nil { + state.Attributes = make(map[string]any) } - s.Attributes[name] = value + state.Attributes[name] = value - return s + return state } } // WithDataSourceAttribute will set the "data_source" additional attribute to // the given value. func WithDataSourceAttribute(source string) Option[State] { - return func(s State) State { - if s.Attributes == nil { - s.Attributes = make(map[string]any) + return func(state State) State { + if state.Attributes == nil { + state.Attributes = make(map[string]any) } - s.Attributes["data_source"] = source + state.Attributes["data_source"] = source - return s + return state } } // WithIcon sets the sensor icon. func WithIcon(icon string) Option[State] { - return func(s State) State { - s.Icon = icon - return s + return func(state State) State { + state.Icon = icon + return state } } // WithID sets the entity ID of the sensor. func WithID(id string) Option[State] { - return func(s State) State { - s.ID = id - return s + return func(state State) State { + state.ID = id + return state } } // AsTypeSensor ensures the sensor is treated as a Sensor Entity. // https://developers.home-assistant.io/docs/core/entity/sensor/ func AsTypeSensor() Option[State] { - return func(s State) State { - s.EntityType = types.Sensor - return s + return func(state State) State { + state.EntityType = types.Sensor + return state } } // AsTypeBinarySensor ensures the sensor is treated as a Binary Sensor Entity. // https://developers.home-assistant.io/docs/core/entity/binary-sensor func AsTypeBinarySensor() Option[State] { - return func(s State) State { - s.EntityType = types.BinarySensor - return s + return func(state State) State { + state.EntityType = types.BinarySensor + return state } } // UpdateValue will update the sensor state with the given value. -func (e *State) UpdateValue(value any) { - e.Value = value +func (s *State) UpdateValue(value any) { + s.Value = value } // UpdateIcon will update the sensor icon with the given value. -func (e *State) UpdateIcon(icon string) { - e.Icon = icon +func (s *State) UpdateIcon(icon string) { + s.Icon = icon } // UpdateAttribute will set the given attribute to the given value. -func (e *State) UpdateAttribute(key string, value any) { - if e.Attributes == nil { - e.Attributes = make(map[string]any) +func (s *State) UpdateAttribute(key string, value any) { + if s.Attributes == nil { + s.Attributes = make(map[string]any) } - e.Attributes[key] = value + + s.Attributes[key] = value } func (s *State) Validate() error { @@ -149,34 +141,6 @@ func (s *State) Validate() error { return nil } -func (s *State) RequestBody() any { - return &Request{ - RequestType: requestTypeUpdateSensor, - Data: s, - } -} - -func (s *State) Retry() bool { - return s.RetryRequest -} - -//nolint:wrapcheck -func (s *State) MarshalJSON() ([]byte, error) { - return json.Marshal(&struct { - State any `json:"state" validate:"required"` - Attributes map[string]any `json:"attributes,omitempty" validate:"omitempty"` - Icon string `json:"icon,omitempty" validate:"omitempty,startswith=mdi:"` - ID string `json:"unique_id" validate:"required"` - EntityType string `json:"type" validate:"omitempty"` - }{ - State: s.Value, - Attributes: s.Attributes, - Icon: s.Icon, - ID: s.ID, - EntityType: s.EntityType.String(), - }) -} - type Entity struct { *State Name string `json:"name" validate:"required"` @@ -187,34 +151,34 @@ type Entity struct { } // WithState sets the sensor state options. This is useful on entity -// creation to set an intial state. +// creation to set an initial state. func WithState(options ...Option[State]) Option[Entity] { - return func(e Entity) Entity { + return func(entity Entity) Entity { state := State{} for _, option := range options { state = option(state) } - e.State = &state + entity.State = &state - return e + return entity } } // WithName sets the friendly name for the sensor entity. func WithName(name string) Option[Entity] { - return func(e Entity) Entity { - e.Name = name - return e + return func(entity Entity) Entity { + entity.Name = name + return entity } } // WithUnits defines the native unit of measurement of the sensor entity. func WithUnits(units string) Option[Entity] { - return func(e Entity) Entity { - e.Units = units - return e + return func(entity Entity) Entity { + entity.Units = units + return entity } } @@ -224,27 +188,27 @@ func WithUnits(units string) Option[Entity] { // // For type Binary Sensor: https://developers.home-assistant.io/docs/core/entity/binary-sensor#available-device-classes func WithDeviceClass(class types.DeviceClass) Option[Entity] { - return func(e Entity) Entity { - e.DeviceClass = class - return e + return func(entity Entity) Entity { + entity.DeviceClass = class + return entity } } // WithStateClass sets the state class of the sensor entity. // https://developers.home-assistant.io/docs/core/entity/sensor/#available-state-classes func WithStateClass(class types.StateClass) Option[Entity] { - return func(e Entity) Entity { - e.StateClass = class - return e + return func(entity Entity) Entity { + entity.StateClass = class + return entity } } // AsDiagnostic sets the sensor entity as a diagnostic. This will ensure it will // be grouped under a diagnostic header in the Home Assistant UI. func AsDiagnostic() Option[Entity] { - return func(e Entity) Entity { - e.Category = types.CategoryDiagnostic - return e + return func(entity Entity) Entity { + entity.Category = types.CategoryDiagnostic + return entity } } @@ -280,44 +244,6 @@ func (e *Entity) Validate() error { return nil } -func (e *Entity) RequestBody() any { - return &Request{ - RequestType: requestTypeRegisterSensor, - Data: e, - } -} - -func (e *Entity) Retry() bool { - return e.RetryRequest -} - -//nolint:wrapcheck -func (e *Entity) MarshalJSON() ([]byte, error) { - return json.Marshal(&struct { - State any `json:"state" validate:"required"` - Attributes map[string]any `json:"attributes,omitempty" validate:"omitempty"` - Icon string `json:"icon,omitempty" validate:"omitempty,startswith=mdi:"` - ID string `json:"unique_id" validate:"required"` - EntityType string `json:"type" validate:"omitempty"` - Name string `json:"name" validate:"required"` - Units string `json:"unit_of_measurement,omitempty" validate:"omitempty"` - DeviceClass string `json:"device_class,omitempty" validate:"omitempty"` - StateClass string `json:"state_class,omitempty" validate:"omitempty"` - Category string `json:"entity_category,omitempty" validate:"omitempty"` - }{ - State: e.Value, - Attributes: e.Attributes, - Icon: e.Icon, - ID: e.ID, - EntityType: e.EntityType.String(), - Name: e.Name, - Units: e.Units, - DeviceClass: e.DeviceClass.String(), - StateClass: e.StateClass.String(), - Category: e.Category.String(), - }) -} - // 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 @@ -340,14 +266,3 @@ func (l *Location) Validate() error { return nil } - -func (l *Location) RequestBody() any { - return &Request{ - RequestType: requestTypeLocation, - Data: l, - } -} - -func (l *Location) Retry() bool { - return false -} diff --git a/internal/hass/sensor/requests.go b/internal/hass/sensor/requests.go new file mode 100644 index 00000000..3298385f --- /dev/null +++ b/internal/hass/sensor/requests.go @@ -0,0 +1,110 @@ +// Copyright 2024 Joshua Rich . +// SPDX-License-Identifier: MIT + +package sensor + +const ( + requestTypeRegisterSensor = "register_sensor" + requestTypeUpdateSensor = "update_sensor_states" + requestTypeLocation = "update_location" +) + +// Request represents a sensor request, either a registration, update or +// location update. +type Request struct { + Data any `json:"data"` + RequestType string `json:"type"` + retryable bool +} + +func (r *Request) RequestBody() any { + return r +} + +func (r *Request) Retry() bool { + return r.retryable +} + +// AsSensorUpdate indicates the request will be a sensor update request. +func AsSensorUpdate(entity Entity) Option[Request] { + return func(request Request) Request { + request.RequestType = requestTypeUpdateSensor + request.Data = &struct { + State any `json:"state" validate:"required"` + Attributes map[string]any `json:"attributes,omitempty" validate:"omitempty"` + Icon string `json:"icon,omitempty" validate:"omitempty,startswith=mdi:"` + ID string `json:"unique_id" validate:"required"` + EntityType string `json:"type" validate:"omitempty"` + }{ + State: entity.Value, + Attributes: entity.Attributes, + Icon: entity.Icon, + ID: entity.ID, + EntityType: entity.EntityType.String(), + } + + return request + } +} + +// AsSensorRegistration indicates the request will be a sensor registration +// request. +func AsSensorRegistration(entity Entity) Option[Request] { + return func(request Request) Request { + request.RequestType = requestTypeRegisterSensor + request.Data = &struct { + State any `json:"state" validate:"required"` + Attributes map[string]any `json:"attributes,omitempty" validate:"omitempty"` + Icon string `json:"icon,omitempty" validate:"omitempty,startswith=mdi:"` + ID string `json:"unique_id" validate:"required"` + EntityType string `json:"type" validate:"omitempty"` + Name string `json:"name" validate:"required"` + Units string `json:"unit_of_measurement,omitempty" validate:"omitempty"` + DeviceClass string `json:"device_class,omitempty" validate:"omitempty"` + StateClass string `json:"state_class,omitempty" validate:"omitempty"` + Category string `json:"entity_category,omitempty" validate:"omitempty"` + }{ + State: entity.Value, + Attributes: entity.Attributes, + Icon: entity.Icon, + ID: entity.ID, + EntityType: entity.EntityType.String(), + Name: entity.Name, + Units: entity.Units, + DeviceClass: entity.DeviceClass.String(), + StateClass: entity.StateClass.String(), + Category: entity.Category.String(), + } + + return request + } +} + +// AsLocationUpdate indicates the request will be a location update request. +func AsLocationUpdate(location Location) Option[Request] { + return func(request Request) Request { + request.RequestType = requestTypeLocation + request.Data = &location + + return request + } +} + +// AsRetryable marks that the request should be retried. +func AsRetryable(value bool) Option[Request] { + return func(request Request) Request { + request.retryable = value + return request + } +} + +// NewRequest creates a new request with the given options. +func NewRequest(options ...Option[Request]) *Request { + request := Request{} + + for _, option := range options { + request = option(request) + } + + return &request +}