package main import ( "bytes" "encoding/json" "fmt" "log" "net/http" "os" "strconv" "sync" "time" "github.com/gin-gonic/gin" ) type Config struct { Dawarich struct { URL string Token string } HomeAssistant struct { URL string Token string DeviceTrackerID string } Auth struct { Username string Password string } Port string } type GPSData struct { Lat string `json:"lat" form:"lat"` Lon string `json:"lon" form:"lon"` Time string `json:"time" form:"time"` Alt string `json:"alt" form:"alt"` Spd string `json:"spd" form:"spd"` Dir string `json:"dir" form:"dir"` Acc string `json:"acc" form:"acc"` Batt string `json:"batt" form:"batt"` } type DawarichLocation struct { Type string `json:"type"` Geometry struct { Type string `json:"type"` Coordinates []float64 `json:"coordinates"` } `json:"geometry"` Properties struct { Timestamp string `json:"timestamp"` Altitude *float64 `json:"altitude,omitempty"` Speed *float64 `json:"speed,omitempty"` Bearing *float64 `json:"bearing,omitempty"` Accuracy *float64 `json:"accuracy,omitempty"` BatteryLevel *float64 `json:"battery_level,omitempty"` } `json:"properties"` } type DawarichPayload struct { Locations []DawarichLocation `json:"locations"` } type HAState struct { State string `json:"state"` Attributes map[string]interface{} `json:"attributes"` } type ForwardResult struct { Success bool `json:"success"` Error string `json:"error,omitempty"` } type Response struct { Timestamp string `json:"timestamp"` ReceivedData GPSData `json:"received_data"` Results struct { Dawarich ForwardResult `json:"dawarich"` HomeAssistant ForwardResult `json:"homeassistant"` } `json:"results"` } var config Config var httpClient = &http.Client{ Timeout: 10 * time.Second, } func loadConfig() { config.Dawarich.URL = os.Getenv("DAWARICH_URL") config.Dawarich.Token = os.Getenv("DAWARICH_TOKEN") config.HomeAssistant.URL = os.Getenv("HOMEASSISTANT_URL") config.HomeAssistant.Token = os.Getenv("HOMEASSISTANT_TOKEN") config.HomeAssistant.DeviceTrackerID = getEnvOrDefault("HA_DEVICE_TRACKER_ID", "gps_logger") config.Auth.Username = os.Getenv("AUTH_USERNAME") config.Auth.Password = os.Getenv("AUTH_PASSWORD") config.Port = getEnvOrDefault("PORT", "3069") } func getEnvOrDefault(key, defaultValue string) string { if value := os.Getenv(key); value != "" { return value } return defaultValue } func parseFloat(s string) *float64 { if s == "" { return nil } if f, err := strconv.ParseFloat(s, 64); err == nil { return &f } return nil } func forwardToDawarich(gpsData GPSData) ForwardResult { if config.Dawarich.URL == "" || config.Dawarich.Token == "" { return ForwardResult{Success: false, Error: "Not configured"} } lat, err := strconv.ParseFloat(gpsData.Lat, 64) if err != nil { return ForwardResult{Success: false, Error: "Invalid latitude"} } lon, err := strconv.ParseFloat(gpsData.Lon, 64) if err != nil { return ForwardResult{Success: false, Error: "Invalid longitude"} } timestamp := gpsData.Time if timestamp == "" { timestamp = time.Now().Format(time.RFC3339) } location := DawarichLocation{ Type: "Feature", Geometry: struct { Type string `json:"type"` Coordinates []float64 `json:"coordinates"` }{ Type: "Point", Coordinates: []float64{lon, lat}, }, Properties: struct { Timestamp string `json:"timestamp"` Altitude *float64 `json:"altitude,omitempty"` Speed *float64 `json:"speed,omitempty"` Bearing *float64 `json:"bearing,omitempty"` Accuracy *float64 `json:"accuracy,omitempty"` BatteryLevel *float64 `json:"battery_level,omitempty"` }{ Timestamp: timestamp, Altitude: parseFloat(gpsData.Alt), Speed: parseFloat(gpsData.Spd), Bearing: parseFloat(gpsData.Dir), Accuracy: parseFloat(gpsData.Acc), BatteryLevel: parseFloat(gpsData.Batt), }, } payload := DawarichPayload{ Locations: []DawarichLocation{location}, } jsonData, err := json.Marshal(payload) if err != nil { return ForwardResult{Success: false, Error: "JSON marshal error"} } req, err := http.NewRequest("POST", config.Dawarich.URL+"/api/v1/overland/batches", bytes.NewBuffer(jsonData)) if err != nil { return ForwardResult{Success: false, Error: "Request creation error"} } req.Header.Set("Authorization", "Bearer "+config.Dawarich.Token) req.Header.Set("Content-Type", "application/json") resp, err := httpClient.Do(req) if err != nil { return ForwardResult{Success: false, Error: err.Error()} } defer resp.Body.Close() if resp.StatusCode >= 200 && resp.StatusCode < 300 { log.Println("Successfully forwarded to Dawarich") return ForwardResult{Success: true} } return ForwardResult{Success: false, Error: fmt.Sprintf("HTTP %d", resp.StatusCode)} } func forwardToHomeAssistant(gpsData GPSData) ForwardResult { if config.HomeAssistant.URL == "" || config.HomeAssistant.Token == "" { return ForwardResult{Success: false, Error: "Not configured"} } lat, err := strconv.ParseFloat(gpsData.Lat, 64) if err != nil { return ForwardResult{Success: false, Error: "Invalid latitude"} } lon, err := strconv.ParseFloat(gpsData.Lon, 64) if err != nil { return ForwardResult{Success: false, Error: "Invalid longitude"} } attributes := map[string]interface{}{ "latitude": lat, "longitude": lon, "source_type": "gps", "friendly_name": "GPS Logger Device", } if acc := parseFloat(gpsData.Acc); acc != nil { attributes["gps_accuracy"] = *acc } if alt := parseFloat(gpsData.Alt); alt != nil { attributes["altitude"] = *alt } if spd := parseFloat(gpsData.Spd); spd != nil { attributes["speed"] = *spd } if dir := parseFloat(gpsData.Dir); dir != nil { attributes["bearing"] = *dir } if batt := parseFloat(gpsData.Batt); batt != nil { attributes["battery_level"] = *batt } state := HAState{ State: "home", Attributes: attributes, } jsonData, err := json.Marshal(state) if err != nil { return ForwardResult{Success: false, Error: "JSON marshal error"} } url := fmt.Sprintf("%s/api/states/device_tracker.%s", config.HomeAssistant.URL, config.HomeAssistant.DeviceTrackerID) req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) if err != nil { return ForwardResult{Success: false, Error: "Request creation error"} } req.Header.Set("Authorization", "Bearer "+config.HomeAssistant.Token) req.Header.Set("Content-Type", "application/json") resp, err := httpClient.Do(req) if err != nil { return ForwardResult{Success: false, Error: err.Error()} } defer resp.Body.Close() if resp.StatusCode >= 200 && resp.StatusCode < 300 { log.Println("Successfully forwarded to Home Assistant") return ForwardResult{Success: true} } return ForwardResult{Success: false, Error: fmt.Sprintf("HTTP %d", resp.StatusCode)} } func basicAuth() gin.HandlerFunc { if config.Auth.Username == "" || config.Auth.Password == "" { return gin.HandlerFunc(func(c *gin.Context) { c.Next() }) } return gin.BasicAuth(gin.Accounts{ config.Auth.Username: config.Auth.Password, }) } func healthHandler(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "status": "healthy", "timestamp": time.Now().Format(time.RFC3339), "config": gin.H{ "dawarich_configured": config.Dawarich.URL != "" && config.Dawarich.Token != "", "homeassistant_configured": config.HomeAssistant.URL != "" && config.HomeAssistant.Token != "", "auth_enabled": config.Auth.Username != "" && config.Auth.Password != "", }, }) } func gpsHandler(c *gin.Context) { var gpsData GPSData // Bind data from both query parameters and body if err := c.ShouldBind(&gpsData); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid GPS data"}) return } log.Printf("Received GPS data: lat=%s, lon=%s", gpsData.Lat, gpsData.Lon) // Validate required fields if gpsData.Lat == "" || gpsData.Lon == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Missing required fields: lat, lon"}) return } // Forward to both services concurrently var wg sync.WaitGroup var dawarichResult, haResult ForwardResult wg.Add(2) go func() { defer wg.Done() dawarichResult = forwardToDawarich(gpsData) }() go func() { defer wg.Done() haResult = forwardToHomeAssistant(gpsData) }() wg.Wait() response := Response{ Timestamp: time.Now().Format(time.RFC3339), ReceivedData: gpsData, Results: struct { Dawarich ForwardResult `json:"dawarich"` HomeAssistant ForwardResult `json:"homeassistant"` }{ Dawarich: dawarichResult, HomeAssistant: haResult, }, } // Return success if at least one service succeeded statusCode := http.StatusInternalServerError if dawarichResult.Success || haResult.Success { statusCode = http.StatusOK } c.JSON(statusCode, response) } func main() { loadConfig() // Set Gin to release mode for production if os.Getenv("GIN_MODE") == "" { gin.SetMode(gin.ReleaseMode) } r := gin.New() r.Use(gin.Logger(), gin.Recovery()) // Health check endpoint (no auth required) r.GET("/health", healthHandler) // Protected endpoints auth := r.Group("/", basicAuth()) { auth.POST("/gps", gpsHandler) } log.Printf("GPS Router service starting on port %s", config.Port) log.Printf("Configuration status:") log.Printf("- Dawarich: %t", config.Dawarich.URL != "" && config.Dawarich.Token != "") log.Printf("- Home Assistant: %t", config.HomeAssistant.URL != "" && config.HomeAssistant.Token != "") log.Printf("- Authentication: %t", config.Auth.Username != "" && config.Auth.Password != "") if err := r.Run(":" + config.Port); err != nil { log.Fatal("Failed to start server:", err) } }