Skip to content

Commit

Permalink
feat(hass): ✨ use options pattern to create sensor requests
Browse files Browse the repository at this point in the history
- use the options pattern when creating sensor requests. Supports all request types (registration, update and location)
  • Loading branch information
joshuar committed Dec 22, 2024
1 parent b614ec3 commit 73f218b
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 147 deletions.
17 changes: 14 additions & 3 deletions internal/hass/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand Down
203 changes: 59 additions & 144 deletions internal/hass/sensor/entities.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
package sensor

import (
"encoding/json"
"fmt"
"maps"

Expand All @@ -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
}
Expand All @@ -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 {
Expand All @@ -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"`
Expand All @@ -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
}
}

Expand All @@ -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
}
}

Expand Down Expand Up @@ -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
Expand All @@ -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
}
Loading

0 comments on commit 73f218b

Please sign in to comment.