2023-02-28 07:06:06 +00:00
|
|
|
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()
|
2023-03-05 08:06:14 +00:00
|
|
|
year_result.Value = roundFloat(year_distance.Value*365/float64(elapsed_days), 2)
|
2023-02-28 07:06:06 +00:00
|
|
|
} 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()
|
|
|
|
}
|