Add build and make scripts
This commit is contained in:
151
main.go
151
main.go
@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@ -11,22 +12,39 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
_ "modernc.org/sqlite" // lightweight pure-Go SQLite driver
|
||||
"gocv.io/x/gocv"
|
||||
)
|
||||
|
||||
type CloudResult struct {
|
||||
ID int `json:"id"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
CloudCover float64 `json:"cloud_cover"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
imageURL = getEnv("CLOUD_IMAGE_URL", "http://clouds.local:8082/snapshot")
|
||||
refreshSecs = getEnvInt("CLOUD_REFRESH_SECONDS", 30)
|
||||
trendHistory []CloudResult
|
||||
trendLock sync.Mutex
|
||||
imageURL = getEnv("CLOUD_IMAGE_URL", "http://clouds.local:8082/snapshot")
|
||||
refreshSecs = getEnvInt("CLOUD_REFRESH_SECONDS", 30)
|
||||
dbPath = getEnv("CLOUD_DB_PATH", "clouds.db")
|
||||
|
||||
db *sql.DB
|
||||
lock sync.Mutex
|
||||
)
|
||||
|
||||
func main() {
|
||||
initDB()
|
||||
go backgroundCollector()
|
||||
|
||||
http.HandleFunc("/", dashboardHandler)
|
||||
http.HandleFunc("/cloudcover", handleLatest)
|
||||
http.HandleFunc("/cloudtrend", handleTrend)
|
||||
|
||||
fmt.Printf("☁️ Cloud cover API started at http://localhost:8080\n")
|
||||
fmt.Printf("🔁 Refresh every %ds | Source: %s | DB: %s\n", refreshSecs, imageURL, dbPath)
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
||||
|
||||
func estimateCloudCover(url string) (float64, error) {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
@ -64,7 +82,6 @@ func estimateCloudCover(url string) (float64, error) {
|
||||
defer valMask.Close()
|
||||
defer cloudMask.Close()
|
||||
|
||||
// Low saturation, high brightness = likely clouds
|
||||
gocv.InRangeWithScalar(s, gocv.NewScalar(0, 0, 0, 0), gocv.NewScalar(60, 0, 0, 0), &satMask)
|
||||
gocv.InRangeWithScalar(v, gocv.NewScalar(120, 0, 0, 0), gocv.NewScalar(255, 0, 0, 0), &valMask)
|
||||
gocv.BitwiseAnd(satMask, valMask, &cloudMask)
|
||||
@ -88,64 +105,96 @@ func backgroundCollector() {
|
||||
result.Error = err.Error()
|
||||
}
|
||||
|
||||
trendLock.Lock()
|
||||
trendHistory = append(trendHistory, result)
|
||||
if len(trendHistory) > 200 {
|
||||
trendHistory = trendHistory[len(trendHistory)-200:]
|
||||
}
|
||||
trendLock.Unlock()
|
||||
|
||||
storeResult(result)
|
||||
fmt.Printf("[%s] Cloud cover: %.2f%%\n", result.Timestamp.Format("15:04:05"), result.CloudCover)
|
||||
time.Sleep(time.Duration(refreshSecs) * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func getEnv(key, def string) string {
|
||||
if v, ok := os.LookupEnv(key); ok {
|
||||
return v
|
||||
func initDB() {
|
||||
var err error
|
||||
db, err = sql.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open database: %v", err)
|
||||
}
|
||||
_, err = db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS cloud_trend (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp DATETIME,
|
||||
cloud_cover REAL,
|
||||
error TEXT
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create table: %v", err)
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func getEnvInt(key string, def int) int {
|
||||
if v, ok := os.LookupEnv(key); ok {
|
||||
if i, err := strconv.Atoi(v); err == nil {
|
||||
return i
|
||||
func storeResult(r CloudResult) {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
_, err := db.Exec(`INSERT INTO cloud_trend (timestamp, cloud_cover, error) VALUES (?, ?, ?)`,
|
||||
r.Timestamp.Format(time.RFC3339), r.CloudCover, r.Error)
|
||||
if err != nil {
|
||||
log.Printf("failed to insert record: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func getLatest() (*CloudResult, error) {
|
||||
row := db.QueryRow(`SELECT id, timestamp, cloud_cover, error FROM cloud_trend ORDER BY id DESC LIMIT 1`)
|
||||
var r CloudResult
|
||||
var ts string
|
||||
if err := row.Scan(&r.ID, &ts, &r.CloudCover, &r.Error); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Timestamp, _ = time.Parse(time.RFC3339, ts)
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
func getAll(limit int) ([]CloudResult, error) {
|
||||
rows, err := db.Query(`SELECT id, timestamp, cloud_cover, error FROM cloud_trend ORDER BY id DESC LIMIT ?`, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var results []CloudResult
|
||||
for rows.Next() {
|
||||
var r CloudResult
|
||||
var ts string
|
||||
if err := rows.Scan(&r.ID, &ts, &r.CloudCover, &r.Error); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Timestamp, _ = time.Parse(time.RFC3339, ts)
|
||||
results = append(results, r)
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func main() {
|
||||
go backgroundCollector()
|
||||
|
||||
http.HandleFunc("/", dashboardHandler)
|
||||
http.HandleFunc("/cloudcover", handleLatest)
|
||||
http.HandleFunc("/cloudtrend", handleTrend)
|
||||
|
||||
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))
|
||||
// Reverse to chronological order
|
||||
for i, j := 0, len(results)-1; i < j; i, j = i+1, j-1 {
|
||||
results[i], results[j] = results[j], results[i]
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func handleLatest(w http.ResponseWriter, r *http.Request) {
|
||||
trendLock.Lock()
|
||||
defer trendLock.Unlock()
|
||||
|
||||
if len(trendHistory) == 0 {
|
||||
http.Error(w, "No data collected yet", http.StatusServiceUnavailable)
|
||||
result, err := getLatest()
|
||||
if err != nil {
|
||||
http.Error(w, "no data yet", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
latest := trendHistory[len(trendHistory)-1]
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(latest)
|
||||
json.NewEncoder(w).Encode(result)
|
||||
}
|
||||
|
||||
func handleTrend(w http.ResponseWriter, r *http.Request) {
|
||||
trendLock.Lock()
|
||||
defer trendLock.Unlock()
|
||||
results, err := getAll(200)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(trendHistory)
|
||||
json.NewEncoder(w).Encode(results)
|
||||
}
|
||||
|
||||
func dashboardHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@ -163,7 +212,7 @@ func dashboardHandler(w http.ResponseWriter, r *http.Request) {
|
||||
</head>
|
||||
<body>
|
||||
<h1>☁️ Cloud Cover Trend</h1>
|
||||
<p>Live view of sky cloudiness from <code>` + imageURL + `</code></p>
|
||||
<p>Source: <code>` + imageURL + `</code></p>
|
||||
<canvas id="chart" width="800" height="400"></canvas>
|
||||
|
||||
<script>
|
||||
@ -209,3 +258,19 @@ func dashboardHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(html))
|
||||
}
|
||||
|
||||
func getEnv(key, def string) string {
|
||||
if v, ok := os.LookupEnv(key); ok {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user