210 lines
5.1 KiB
Go
210 lines
5.1 KiB
Go
// Copyright (c) 2024 Joshua Rich <joshua.rich@gmail.com>
|
|
//
|
|
// This software is released under the MIT License.
|
|
// https://opensource.org/licenses/MIT
|
|
|
|
//revive:disable:max-public-structs
|
|
//go:generate moq -out sensor_mocks_test.go . State Registration Details
|
|
package sensor
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/joshuar/go-hass-agent/internal/hass/sensor/types"
|
|
)
|
|
|
|
const (
|
|
StateUnknown = "Unknown"
|
|
|
|
CategoryDiagnostic = "diagnostic"
|
|
|
|
RequestTypeRegister = "register_sensor"
|
|
RequestTypeUpdate = "update_sensor_states"
|
|
RequestTypeLocation = "update_location"
|
|
)
|
|
|
|
var (
|
|
ErrNotLocation = errors.New("sensor details do not represent a location update")
|
|
ErrUnknownDetails = errors.New("unknown sensor details")
|
|
)
|
|
|
|
type State interface {
|
|
ID() string
|
|
Icon() string
|
|
State() any
|
|
SensorType() types.SensorClass
|
|
Units() string
|
|
Attributes() map[string]any
|
|
}
|
|
|
|
type Registration interface {
|
|
State
|
|
Name() string
|
|
DeviceClass() types.DeviceClass
|
|
StateClass() types.StateClass
|
|
Category() string
|
|
}
|
|
|
|
type Details interface {
|
|
State
|
|
Registration
|
|
}
|
|
|
|
type stateUpdateRequest struct {
|
|
StateAttributes map[string]any `json:"attributes,omitempty"`
|
|
State any `json:"state" validate:"required"`
|
|
Icon string `json:"icon,omitempty" validate:"startswith=mdi"`
|
|
Type string `json:"type"`
|
|
UniqueID string `json:"unique_id" validate:"required"`
|
|
}
|
|
|
|
func createStateUpdateRequest(sensor State) *stateUpdateRequest {
|
|
return &stateUpdateRequest{
|
|
StateAttributes: sensor.Attributes(),
|
|
State: sensor.State(),
|
|
Icon: sensor.Icon(),
|
|
UniqueID: sensor.ID(),
|
|
Type: sensor.SensorType().String(),
|
|
}
|
|
}
|
|
|
|
type registrationRequest struct {
|
|
*stateUpdateRequest
|
|
Name string `json:"name" validate:"required"`
|
|
UnitOfMeasurement string `json:"unit_of_measurement,omitempty"`
|
|
StateClass string `json:"state_class,omitempty"`
|
|
EntityCategory string `json:"entity_category,omitempty"`
|
|
DeviceClass string `json:"device_class,omitempty"`
|
|
}
|
|
|
|
func createRegistrationRequest(sensor Registration) *registrationRequest {
|
|
reg := ®istrationRequest{
|
|
stateUpdateRequest: createStateUpdateRequest(sensor),
|
|
Name: sensor.Name(),
|
|
UnitOfMeasurement: sensor.Units(),
|
|
EntityCategory: sensor.Category(),
|
|
}
|
|
|
|
if sensor.StateClass() > 0 {
|
|
reg.StateClass = sensor.StateClass().String()
|
|
}
|
|
|
|
if sensor.DeviceClass() > 0 {
|
|
reg.DeviceClass = sensor.DeviceClass().String()
|
|
}
|
|
|
|
return reg
|
|
}
|
|
|
|
// LocationRequest 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
|
|
// differently from other sensors.
|
|
type LocationRequest struct {
|
|
Gps []float64 `json:"gps"`
|
|
GpsAccuracy int `json:"gps_accuracy,omitempty"`
|
|
Battery int `json:"battery,omitempty"`
|
|
Speed int `json:"speed,omitempty"`
|
|
Altitude int `json:"altitude,omitempty"`
|
|
Course int `json:"course,omitempty"`
|
|
VerticalAccuracy int `json:"vertical_accuracy,omitempty"`
|
|
}
|
|
|
|
type Request struct {
|
|
Data any `json:"data"`
|
|
RequestType string `json:"type" validate:"oneof=register_sensor update_sensor_states update_location"`
|
|
}
|
|
|
|
func (r *Request) Validate() error {
|
|
err := validate.Struct(r.Data)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %s", ErrValidationFailed, parseValidationErrors(err))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *Request) RequestBody() json.RawMessage {
|
|
data, err := json.Marshal(r)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
return json.RawMessage(data)
|
|
}
|
|
|
|
func NewRequest(reqType string, details Details) (*Request, error) {
|
|
var reqBody any
|
|
|
|
switch reqType {
|
|
case RequestTypeRegister:
|
|
reqBody = createRegistrationRequest(details)
|
|
case RequestTypeUpdate:
|
|
reqBody = createStateUpdateRequest(details)
|
|
case RequestTypeLocation:
|
|
if location, ok := details.State().(*LocationRequest); !ok {
|
|
return nil, ErrNotLocation
|
|
} else {
|
|
reqBody = location
|
|
}
|
|
default:
|
|
return nil, ErrUnknownDetails
|
|
}
|
|
|
|
return &Request{Data: reqBody, RequestType: reqType}, nil
|
|
}
|
|
|
|
type APIError struct {
|
|
Code any `json:"code,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
func (e *APIError) Error() string {
|
|
return fmt.Sprintf("code %v: %s", e.Code, e.Message)
|
|
}
|
|
|
|
type ResponseStatus struct {
|
|
ErrorDetails *APIError
|
|
IsSuccess bool `json:"success,omitempty"`
|
|
}
|
|
|
|
type UpdateResponseStatus struct {
|
|
ResponseStatus
|
|
IsDisabled bool `json:"is_disabled,omitempty"`
|
|
}
|
|
|
|
func (u *UpdateResponseStatus) Disabled() bool {
|
|
return u.IsDisabled
|
|
}
|
|
|
|
func (u *UpdateResponseStatus) Success() (bool, error) {
|
|
if u.IsSuccess {
|
|
return true, nil
|
|
}
|
|
|
|
return false, u.ErrorDetails
|
|
}
|
|
|
|
type StateUpdateResponse map[string]UpdateResponseStatus
|
|
|
|
type RegistrationResponse ResponseStatus
|
|
|
|
func (r *RegistrationResponse) Registered() (bool, error) {
|
|
if r.IsSuccess {
|
|
return true, nil
|
|
}
|
|
|
|
return false, r.ErrorDetails
|
|
}
|
|
|
|
type LocationResponse struct {
|
|
error
|
|
}
|
|
|
|
//nolint:staticcheck
|
|
func (r *LocationResponse) Updated() error {
|
|
return r
|
|
}
|