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" config_path string = "./config.json" ) func init() { logger.SetPrefix("apiservice/bike: ") configure() } 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() relation := float64(elapsed_days) * 100 / 365 year_result.Value = roundFloat(year_distance.Value*relation, 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() }