bicycle/bicycle.go

243 lines
5.8 KiB
Go

package bicycle
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"math"
"net/http"
"os"
"path"
"strconv"
"sync"
"time"
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
)
type drive struct {
StartTime time.Time
TotalDistance float64
}
type distance struct {
Value float64 `json:"value"`
Unit string `json:"unit"`
}
type year_collect struct {
TotalDistance distance `json:"total_distance"`
YearResult distance `json:"year_result"`
Months map[string]distance `json:"months"`
}
type api_cycle struct {
Years map[string]year_collect
}
type config struct {
InfluxUrl string `json:"influx_url"`
InfluxToken string `json:"influx_token"`
InfluxBucket string `json:"influx_bucket"`
InfluxOrg string `json:"influx_org"`
}
const (
DISTANCE_UNIT = "km"
)
var (
logger log.Logger = *log.Default()
cycle_api api_cycle
api_mutex sync.Mutex
config_cache = config{ // Default config - written if not existing @ config_path
InfluxUrl: "http://nuc:8086",
InfluxToken: "",
InfluxBucket: "home",
InfluxOrg: "tkl",
}
config_path string = "/var/lib/apiservice/bicycle/config.json"
)
func init() {
logger.SetPrefix("apiservice/bike: ")
}
func configure() {
_, err := os.Stat(config_path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
logger.Printf("Config file does not exist at '%s'. Creating one...", config_path)
// Create config directory
dir := path.Dir(config_path)
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
logger.Fatal(err)
}
file, err := os.OpenFile(config_path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
logger.Fatal(err)
}
defer file.Close()
content, err := json.Marshal(config_cache)
if err != nil {
logger.Fatal(err)
}
file.Write(content)
return
} else {
logger.Fatal(err)
}
}
data, err := os.ReadFile(config_path)
if err != nil {
logger.Fatal(err)
}
err = json.Unmarshal(data, &config_cache)
if err != nil {
logger.Fatal(err)
}
}
func roundFloat(val float64, precision uint) float64 {
ratio := math.Pow(10, float64(precision))
return math.Round(val*ratio) / ratio
}
func get_total_distances(client *influxdb2.Client, org string, bucket string, start time.Time, stop time.Time) ([]drive, error) {
var res []drive
queryAPI := (*client).QueryAPI(org)
query := fmt.Sprintf(`from(bucket: "%s")
|> range(start: %s, stop: %s)
|> filter(fn: (r) => r._measurement == "activity")
|> filter(fn: (r) => r["_field"] == "TotalDistance")`, bucket, start.Format(time.RFC3339), stop.Format(time.RFC3339))
result, err := queryAPI.Query(context.Background(), query)
if err != nil {
fmt.Print(err)
return nil, err
}
for result.Next() {
var tmp drive
tmp.StartTime = result.Record().Time()
tmp.TotalDistance = result.Record().Value().(float64) / 100000.0
res = append(res, tmp)
}
return res, nil
}
func api_endpoint_cycle(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-type", "application/json; charset=utf-8;")
if r.Method == http.MethodGet {
// build json object of api struct
api_mutex.Lock()
res, err := json.Marshal(cycle_api)
api_mutex.Unlock()
if err != nil {
logger.Print(err)
w.WriteHeader(http.StatusInternalServerError)
w.Write(json.RawMessage(`{"error": "cannot marshal object to json"}`))
} else {
w.WriteHeader(http.StatusOK)
w.Write(res)
}
} else {
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
func api_builder() {
// create influx client
client := influxdb2.NewClient(config_cache.InfluxUrl, config_cache.InfluxToken)
defer client.Close()
for {
// end date for query
stop := time.Now()
// start date for query (1st january of the same year as stop)
start := time.Date(stop.Year(), time.January, 1, 0, 0, 0, 0, time.UTC)
// get TotalDistance(s) between start and stop
distances, err := get_total_distances(&client, config_cache.InfluxOrg, config_cache.InfluxBucket, start, stop)
if err != nil {
logger.Print(err)
time.Sleep(time.Second)
continue
}
// get number of years
var year_list []int
for i := range distances {
year := distances[i].StartTime.Year()
exists := false
for _, value := range year_list {
if value == year {
exists = true
break
}
}
if !exists {
year_list = append(year_list, year)
}
}
api_mutex.Lock()
// init api structs
cycle_api.Years = make(map[string]year_collect)
for _, value := range year_list {
years := cycle_api.Years
months := make(map[string]distance)
var tmp year_collect
tmp.Months = months
years[strconv.Itoa(value)] = tmp
cycle_api.Years = years
}
// take over values into api struct
for i := range distances {
// get nested struct elements
year_key := strconv.Itoa(distances[i].StartTime.Year())
year := cycle_api.Years[year_key]
months := year.Months
year_distance := year.TotalDistance
key := distances[i].StartTime.Month()
entry := months[key.String()]
// takeover values
entry.Value = roundFloat(entry.Value+distances[i].TotalDistance, 2)
entry.Unit = DISTANCE_UNIT
year_distance.Value = roundFloat(year_distance.Value+distances[i].TotalDistance, 2)
year_distance.Unit = DISTANCE_UNIT
// year result
var year_result distance
year_result.Unit = DISTANCE_UNIT
if distances[i].StartTime.Year() == time.Now().Year() {
elapsed_days := time.Now().YearDay()
year_result.Value = roundFloat(year_distance.Value*365/float64(elapsed_days), 2)
} else {
year_result = year_distance
}
// replace nested struct elements
months[key.String()] = entry
year.TotalDistance = year_distance
year.YearResult = year_result
year.Months = months
cycle_api.Years[year_key] = year
}
api_mutex.Unlock()
time.Sleep(time.Minute)
}
}
func Start() {
configure()
http.HandleFunc("/api/bicycle", api_endpoint_cycle)
go api_builder()
}