Files
go-garth/internal/data/body_battery.go
2025-09-22 16:41:49 -07:00

213 lines
5.6 KiB
Go

package data
import (
"encoding/json"
"fmt"
"sort"
"time"
garth "github.com/sstent/go-garth/pkg/garth/types"
shared "github.com/sstent/go-garth/shared/interfaces"
)
// BodyBatteryReading represents a single body battery data point
type BodyBatteryReading struct {
Timestamp int `json:"timestamp"`
Status string `json:"status"`
Level int `json:"level"`
Version float64 `json:"version"`
}
// ParseBodyBatteryReadings converts body battery values array to structured readings
// Accepts mixed numeric types (int, int64, float64, json.Number) for robustness.
func ParseBodyBatteryReadings(valuesArray [][]any) []BodyBatteryReading {
readings := make([]BodyBatteryReading, 0, len(valuesArray))
toInt := func(v any) (int, bool) {
switch t := v.(type) {
case int:
return t, true
case int32:
return int(t), true
case int64:
return int(t), true
case float32:
return int(t), true
case float64:
return int(t), true
case json.Number:
i, err := t.Int64()
if err == nil {
return int(i), true
}
f, err := t.Float64()
if err == nil {
return int(f), true
}
return 0, false
default:
return 0, false
}
}
toFloat64 := func(v any) (float64, bool) {
switch t := v.(type) {
case float32:
return float64(t), true
case float64:
return t, true
case int:
return float64(t), true
case int32:
return float64(t), true
case int64:
return float64(t), true
case json.Number:
f, err := t.Float64()
if err == nil {
return f, true
}
return 0, false
default:
return 0, false
}
}
for _, values := range valuesArray {
if len(values) < 4 {
continue
}
ts, ok1 := toInt(values[0])
status, ok2 := values[1].(string)
lvl, ok3 := toInt(values[2])
ver, ok4 := toFloat64(values[3])
if !ok1 || !ok2 || !ok3 || !ok4 {
continue
}
readings = append(readings, BodyBatteryReading{
Timestamp: ts,
Status: status,
Level: lvl,
Version: ver,
})
}
sort.Slice(readings, func(i, j int) bool {
return readings[i].Timestamp < readings[j].Timestamp
})
return readings
}
// BodyBatteryDataWithMethods embeds garth.DetailedBodyBatteryData and adds methods
type BodyBatteryDataWithMethods struct {
garth.DetailedBodyBatteryData
}
func (d *BodyBatteryDataWithMethods) Get(day time.Time, c shared.APIClient) (interface{}, error) {
dateStr := day.Format("2006-01-02")
// Get main Body Battery data
path1 := fmt.Sprintf("/wellness-service/wellness/dailyStress/%s", dateStr)
data1, err := c.ConnectAPI(path1, "GET", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to get Body Battery stress data: %w", err)
}
// Get Body Battery events
path2 := fmt.Sprintf("/wellness-service/wellness/bodyBattery/%s", dateStr)
data2, err := c.ConnectAPI(path2, "GET", nil, nil)
if err != nil {
// Events might not be available, continue without them
data2 = []byte("[]")
}
var result garth.DetailedBodyBatteryData
if len(data1) > 0 {
if err := json.Unmarshal(data1, &result); err != nil {
return nil, fmt.Errorf("failed to parse Body Battery data: %w", err)
}
}
var events []garth.BodyBatteryEvent
if len(data2) > 0 {
if err := json.Unmarshal(data2, &events); err == nil {
result.Events = events
}
}
return &BodyBatteryDataWithMethods{DetailedBodyBatteryData: result}, nil
}
// GetCurrentLevel returns the most recent Body Battery level
func (d *BodyBatteryDataWithMethods) GetCurrentLevel() int {
if len(d.BodyBatteryValuesArray) == 0 {
return 0
}
readings := ParseBodyBatteryReadings(d.BodyBatteryValuesArray)
if len(readings) == 0 {
return 0
}
return readings[len(readings)-1].Level
}
// GetDayChange returns the Body Battery change for the day
func (d *BodyBatteryDataWithMethods) GetDayChange() int {
readings := ParseBodyBatteryReadings(d.BodyBatteryValuesArray)
if len(readings) < 2 {
return 0
}
return readings[len(readings)-1].Level - readings[0].Level
}
// Added for test compatibility and public API alignment
// DailyBodyBatteryStress wraps garth.DetailedBodyBatteryData and provides a Get method compatible with existing tests.
// See [type DailyBodyBatteryStress](internal/data/body_battery.go:0) and [func (*DailyBodyBatteryStress).Get](internal/data/body_battery.go:0)
type DailyBodyBatteryStress struct {
garth.DetailedBodyBatteryData
}
// Get retrieves Body Battery daily stress data and associated events for a given day.
// Mirrors logic in BodyBatteryDataWithMethods.Get to maintain a consistent behavior.
// Returns (*DailyBodyBatteryStress, nil) on success, (nil, nil) when no data available.
func (d *DailyBodyBatteryStress) Get(day time.Time, c shared.APIClient) (interface{}, error) {
dateStr := day.Format("2006-01-02")
// Get main Body Battery data
path1 := fmt.Sprintf("/wellness-service/wellness/dailyStress/%s", dateStr)
data1, err := c.ConnectAPI(path1, "GET", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to get Body Battery stress data: %w", err)
}
// Get Body Battery events
path2 := fmt.Sprintf("/wellness-service/wellness/bodyBattery/%s", dateStr)
data2, err := c.ConnectAPI(path2, "GET", nil, nil)
if err != nil {
// Events might not be available, continue without them
data2 = []byte("[]")
}
var result garth.DetailedBodyBatteryData
if len(data1) > 0 {
if err := json.Unmarshal(data1, &result); err != nil {
return nil, fmt.Errorf("failed to parse Body Battery data: %w", err)
}
}
var events []garth.BodyBatteryEvent
if len(data2) > 0 {
if err := json.Unmarshal(data2, &events); err == nil {
result.Events = events
}
}
return &DailyBodyBatteryStress{DetailedBodyBatteryData: result}, nil
}