Adding trend endpoint and example envrc
This commit is contained in:
2
envrc.example
Normal file
2
envrc.example
Normal file
@ -0,0 +1,2 @@
|
||||
export CLOUD_IMAGE_URL="http://clouds.local:8082/snapshot"
|
||||
example CLOUD_REFRESH_SECONDS=60
|
||||
227
main.go
227
main.go
@ -1,150 +1,149 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gocv.io/x/gocv"
|
||||
"gocv.io/x/gocv"
|
||||
)
|
||||
|
||||
type CloudResult struct {
|
||||
URL string `json:"url"`
|
||||
CloudCover float64 `json:"cloud_cover"`
|
||||
LastUpdated string `json:"last_updated"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
CloudCover float64 `json:"cloud_cover"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
cache = make(map[string]CloudResult)
|
||||
refreshFreq = 30 * time.Second // refresh interval
|
||||
imageURL = getEnv("CLOUD_IMAGE_URL", "http://clouds.local:8082/snapshot")
|
||||
refreshSecs = getEnvInt("CLOUD_REFRESH_SECONDS", 30)
|
||||
trendHistory []CloudResult
|
||||
trendLock sync.Mutex
|
||||
)
|
||||
|
||||
func estimateCloudCover(url string) (float64, error) {
|
||||
// Fetch image
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to fetch image: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to fetch image: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to read image data: %v", err)
|
||||
}
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to read image: %v", err)
|
||||
}
|
||||
|
||||
img, err := gocv.IMDecode(data, gocv.IMReadColor)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to decode image: %v", err)
|
||||
}
|
||||
defer img.Close()
|
||||
img, err := gocv.IMDecode(data, gocv.IMReadColor)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to decode image: %v", err)
|
||||
}
|
||||
defer img.Close()
|
||||
|
||||
// Convert to HSV
|
||||
hsv := gocv.NewMat()
|
||||
defer hsv.Close()
|
||||
gocv.CvtColor(img, &hsv, gocv.ColorBGRToHSV)
|
||||
hsv := gocv.NewMat()
|
||||
defer hsv.Close()
|
||||
gocv.CvtColor(img, &hsv, gocv.ColorBGRToHSV)
|
||||
|
||||
channels := gocv.Split(hsv)
|
||||
s, v := channels[1], channels[2]
|
||||
defer func() {
|
||||
for _, ch := range channels {
|
||||
ch.Close()
|
||||
}
|
||||
}()
|
||||
channels := gocv.Split(hsv)
|
||||
s, v := channels[1], channels[2]
|
||||
defer func() {
|
||||
for _, ch := range channels {
|
||||
ch.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
satMask := gocv.NewMat()
|
||||
valMask := gocv.NewMat()
|
||||
cloudMask := gocv.NewMat()
|
||||
defer satMask.Close()
|
||||
defer valMask.Close()
|
||||
defer cloudMask.Close()
|
||||
satMask := gocv.NewMat()
|
||||
valMask := gocv.NewMat()
|
||||
cloudMask := gocv.NewMat()
|
||||
defer satMask.Close()
|
||||
defer valMask.Close()
|
||||
defer cloudMask.Close()
|
||||
|
||||
lowerS := gocv.NewMatFromScalar(gocv.NewScalar(0, 0, 0, 0), gocv.MatTypeCV8U)
|
||||
upperS := gocv.NewMatFromScalar(gocv.NewScalar(60, 0, 0, 0), gocv.MatTypeCV8U)
|
||||
lowerV := gocv.NewMatFromScalar(gocv.NewScalar(120, 0, 0, 0), gocv.MatTypeCV8U)
|
||||
upperV := gocv.NewMatFromScalar(gocv.NewScalar(255, 0, 0, 0), gocv.MatTypeCV8U)
|
||||
defer lowerS.Close()
|
||||
defer upperS.Close()
|
||||
defer lowerV.Close()
|
||||
defer upperV.Close()
|
||||
// s < 60 (low saturation)
|
||||
gocv.InRangeWithScalar(s, gocv.NewScalar(0, 0, 0, 0), gocv.NewScalar(60, 0, 0, 0), &satMask)
|
||||
// v > 120 (bright)
|
||||
gocv.InRangeWithScalar(v, gocv.NewScalar(120, 0, 0, 0), gocv.NewScalar(255, 0, 0, 0), &valMask)
|
||||
|
||||
gocv.InRange(s, lowerS, upperS, &satMask)
|
||||
gocv.InRange(v, lowerV, upperV, &valMask)
|
||||
gocv.BitwiseAnd(satMask, valMask, &cloudMask)
|
||||
gocv.BitwiseAnd(satMask, valMask, &cloudMask)
|
||||
|
||||
totalPixels := cloudMask.Rows() * cloudMask.Cols()
|
||||
if totalPixels == 0 {
|
||||
return 0, fmt.Errorf("no pixels found in image")
|
||||
}
|
||||
totalPixels := cloudMask.Rows() * cloudMask.Cols()
|
||||
if totalPixels == 0 {
|
||||
return 0, fmt.Errorf("no pixels found in image")
|
||||
}
|
||||
|
||||
cloudPixels := gocv.CountNonZero(cloudMask)
|
||||
cloudCover := 100 * float64(cloudPixels) / float64(totalPixels)
|
||||
cloudPixels := gocv.CountNonZero(cloudMask)
|
||||
cloudCover := 100 * float64(cloudPixels) / float64(totalPixels)
|
||||
|
||||
return cloudCover, nil
|
||||
return cloudCover, nil
|
||||
}
|
||||
|
||||
func updateCache(url string) {
|
||||
cover, err := estimateCloudCover(url)
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
func backgroundCollector() {
|
||||
for {
|
||||
cover, err := estimateCloudCover(imageURL)
|
||||
result := CloudResult{Timestamp: time.Now(), CloudCover: cover}
|
||||
if err != nil {
|
||||
result.Error = err.Error()
|
||||
}
|
||||
|
||||
result := CloudResult{
|
||||
URL: url,
|
||||
CloudCover: cover,
|
||||
LastUpdated: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
if err != nil {
|
||||
result.Error = err.Error()
|
||||
}
|
||||
trendLock.Lock()
|
||||
trendHistory = append(trendHistory, result)
|
||||
// Keep last 100 entries
|
||||
if len(trendHistory) > 100 {
|
||||
trendHistory = trendHistory[len(trendHistory)-100:]
|
||||
}
|
||||
trendLock.Unlock()
|
||||
|
||||
cache[url] = result
|
||||
fmt.Printf("[%s] Cloud cover: %.2f%%\n", result.Timestamp.Format("15:04:05"), result.CloudCover)
|
||||
time.Sleep(time.Duration(refreshSecs) * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func startAutoRefresh(url string) {
|
||||
go func() {
|
||||
for {
|
||||
updateCache(url)
|
||||
time.Sleep(refreshFreq)
|
||||
}
|
||||
}()
|
||||
func getEnv(key, def string) string {
|
||||
if v, ok := os.LookupEnv(key); ok {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func handleCloudCover(w http.ResponseWriter, r *http.Request) {
|
||||
url := r.URL.Query().Get("url")
|
||||
if url == "" {
|
||||
url = "http://clouds.service:8081/snapshot"
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
result, ok := cache[url]
|
||||
mu.Unlock()
|
||||
|
||||
if !ok {
|
||||
// first-time fetch triggers background updates
|
||||
updateCache(url)
|
||||
startAutoRefresh(url)
|
||||
mu.Lock()
|
||||
result = cache[url]
|
||||
mu.Unlock()
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(result)
|
||||
func getEnvInt(key string, def int) int {
|
||||
if v, ok := os.LookupEnv(key); ok {
|
||||
if i, err := strconv.Atoi(v); err == nil {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/cloudcover", handleCloudCover)
|
||||
http.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("ok"))
|
||||
})
|
||||
go backgroundCollector()
|
||||
|
||||
fmt.Printf("☁️ Cloud cover API running on http://localhost:8080\n")
|
||||
fmt.Printf(" Example: curl 'http://localhost:8080/cloudcover?url=http://clouds.local:8082/snapshot'\n")
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
http.HandleFunc("/cloudcover", func(w http.ResponseWriter, r *http.Request) {
|
||||
trendLock.Lock()
|
||||
defer trendLock.Unlock()
|
||||
|
||||
if len(trendHistory) == 0 {
|
||||
http.Error(w, "No data collected yet", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
latest := trendHistory[len(trendHistory)-1]
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(latest)
|
||||
})
|
||||
|
||||
http.HandleFunc("/cloudtrend", func(w http.ResponseWriter, r *http.Request) {
|
||||
trendLock.Lock()
|
||||
defer trendLock.Unlock()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(trendHistory)
|
||||
})
|
||||
|
||||
fmt.Printf("☁️ Cloud cover API started at http://localhost:8080\n")
|
||||
fmt.Printf("🔁 Refresh interval: %ds | Source: %s\n", refreshSecs, imageURL)
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user