| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749 |
- package controllers
- import (
- "fmt"
- "ems-backend/db"
- "ems-backend/models"
- "encoding/json"
- "net/http"
- "sort"
- "time"
- "github.com/gin-gonic/gin"
- "github.com/google/uuid"
- "gorm.io/datatypes"
- "bytes"
- "io"
- "strings"
- )
- // --- Integration Source Controllers ---
- type HAConfig struct {
- URL string `json:"url"`
- Token string `json:"token"`
- }
- type HAEntity struct {
- EntityID string `json:"entity_id"`
- State string `json:"state"`
- Attributes map[string]interface{} `json:"attributes"`
- LastChanged time.Time `json:"last_changed"`
- LastUpdated time.Time `json:"last_updated"`
- DeviceID string `json:"device_id"` // Augmented field
- DeviceName string `json:"device_name"` // Augmented field
- }
- // HA Template Request
- type HATemplateReq struct {
- Template string `json:"template"`
- }
- // Struct for template result parsing
- type HATemplateResult struct {
- ID string `json:"id"`
- State string `json:"s"`
- Name string `json:"n"`
- DID string `json:"did"`
- DName string `json:"dn"`
- }
- // HADevice represents a Home Assistant Device
- type HADevice struct {
- ID string `json:"id"`
- Name string `json:"name"`
- Model string `json:"model"`
- Manufacturer string `json:"manufacturer"`
- }
- func fetchHADevices(config datatypes.JSON) ([]HADevice, error) {
- var haConfig HAConfig
- b, err := config.MarshalJSON()
- if err != nil {
- return nil, fmt.Errorf("config error: %v", err)
- }
- if err := json.Unmarshal(b, &haConfig); err != nil {
- return nil, fmt.Errorf("invalid configuration format: %v", err)
- }
- if haConfig.URL == "" || haConfig.Token == "" {
- return nil, fmt.Errorf("URL and Token are required")
- }
- client := &http.Client{Timeout: 10 * time.Second}
- url := haConfig.URL
- // Robust URL handling: remove trailing slash and /api suffix
- url = strings.TrimSuffix(url, "/")
- url = strings.TrimSuffix(url, "/api")
-
- // Use Template API to get devices efficiently
- // Simplified template avoiding list.append due to sandbox restrictions
- template := `
- {% set ns = namespace(result=[], devs=[]) %}
- {% for state in states %}
- {% set d = device_id(state.entity_id) %}
- {% if d and d not in ns.devs %}
- {% set ns.devs = ns.devs + [d] %}
- {% set name = device_attr(d, 'name_by_user') %}
- {% if not name %}
- {% set name = device_attr(d, 'name') %}
- {% endif %}
- {% if not name %}
- {% set name = 'Unknown' %}
- {% endif %}
- {% set entry = {
- "id": d,
- "name": name,
- "model": device_attr(d, 'model') or "",
- "manufacturer": device_attr(d, 'manufacturer') or ""
- } %}
- {% set ns.result = ns.result + [entry] %}
- {% endif %}
- {% endfor %}
- {{ ns.result | to_json }}
- `
- reqBody, _ := json.Marshal(HATemplateReq{Template: template})
- req, err := http.NewRequest("POST", url+"/api/template", bytes.NewBuffer(reqBody))
- if err != nil {
- return nil, fmt.Errorf("failed to create request: %v", err)
- }
- req.Header.Set("Authorization", "Bearer "+haConfig.Token)
- req.Header.Set("Content-Type", "application/json")
- resp, err := client.Do(req)
- if err != nil {
- return nil, fmt.Errorf("connection failed: %v", err)
- }
- defer resp.Body.Close()
- // Read body for better error reporting
- bodyBytes, err := io.ReadAll(resp.Body)
- if err != nil {
- return nil, fmt.Errorf("failed to read response body: %v", err)
- }
- if resp.StatusCode != 200 {
- fmt.Printf("DEBUG: HA Status Error: %s, Body: %s\n", resp.Status, string(bodyBytes))
- return nil, fmt.Errorf("Home Assistant returned status: %s. Body: %s", resp.Status, string(bodyBytes))
- }
- var devices []HADevice
- if err := json.Unmarshal(bodyBytes, &devices); err != nil {
- // Try to see if it's because empty result or format
- fmt.Printf("DEBUG: Failed to decode HA response: %s\nError: %v\n", string(bodyBytes), err)
- return nil, fmt.Errorf("failed to decode response: %v. Body: %s", err, string(bodyBytes))
- }
-
- fmt.Printf("DEBUG: Successfully fetched %d devices\n", len(devices))
- return devices, nil
- }
- func fetchHAEntitiesByDevice(config datatypes.JSON, deviceID string) ([]HAEntity, error) {
- var haConfig HAConfig
- b, err := config.MarshalJSON()
- if err != nil {
- return nil, fmt.Errorf("config error: %v", err)
- }
- if err := json.Unmarshal(b, &haConfig); err != nil {
- return nil, fmt.Errorf("invalid configuration format: %v", err)
- }
- if haConfig.URL == "" || haConfig.Token == "" {
- return nil, fmt.Errorf("URL and Token are required")
- }
- client := &http.Client{Timeout: 10 * time.Second}
- url := haConfig.URL
- // Robust URL handling
- url = strings.TrimSuffix(url, "/")
- url = strings.TrimSuffix(url, "/api")
- // Template to fetch entities for a specific device
- // Using strings.Replace to avoid fmt.Sprintf interpreting Jinja2 tags {% as format specifiers
- rawTemplate := `
- {% set ns = namespace(result=[]) %}
- {% set device_entities = device_entities('__DEVICE_ID__') %}
- {% for entity_id in device_entities %}
- {% set state = states[entity_id] %}
- {% if state %}
- {% set name = state.attributes.friendly_name %}
- {% if name is not defined or name is none %}
- {% set name = entity_id %}
- {% endif %}
- {% set entry = {
- "id": entity_id,
- "s": state.state,
- "n": name,
- "did": '__DEVICE_ID__',
- "dn": ''
- } %}
- {% set ns.result = ns.result + [entry] %}
- {% endif %}
- {% endfor %}
- {{ ns.result | to_json }}
- `
- template := strings.ReplaceAll(rawTemplate, "__DEVICE_ID__", deviceID)
- reqBody, _ := json.Marshal(HATemplateReq{Template: template})
- req, err := http.NewRequest("POST", url+"/api/template", bytes.NewBuffer(reqBody))
- if err != nil {
- return nil, fmt.Errorf("failed to create request: %v", err)
- }
- req.Header.Set("Authorization", "Bearer "+haConfig.Token)
- req.Header.Set("Content-Type", "application/json")
- resp, err := client.Do(req)
- if err != nil {
- return nil, fmt.Errorf("connection failed: %v", err)
- }
- defer resp.Body.Close()
- // Read body for better error reporting
- bodyBytes, err := io.ReadAll(resp.Body)
- if err != nil {
- return nil, fmt.Errorf("failed to read response body: %v", err)
- }
- if resp.StatusCode != 200 {
- fmt.Printf("DEBUG: HA Status Error (Entities): %s, Body: %s\n", resp.Status, string(bodyBytes))
- return nil, fmt.Errorf("Home Assistant returned status: %s. Body: %s", resp.Status, string(bodyBytes))
- }
- var tmplResults []HATemplateResult
- if err := json.Unmarshal(bodyBytes, &tmplResults); err != nil {
- fmt.Printf("DEBUG: Failed to decode HA response (Entities): %s\nError: %v\n", string(bodyBytes), err)
- return nil, fmt.Errorf("failed to decode response: %v. Body: %s", err, string(bodyBytes))
- }
-
- fmt.Printf("DEBUG: Successfully fetched %d entities for device %s\n", len(tmplResults), deviceID)
- entities := make([]HAEntity, len(tmplResults))
- for i, r := range tmplResults {
- entities[i] = HAEntity{
- EntityID: r.ID,
- State: r.State,
- Attributes: map[string]interface{}{"friendly_name": r.Name},
- DeviceID: r.DID,
- DeviceName: r.DName,
- }
- }
- // Sort by friendly_name
- sort.Slice(entities, func(i, j int) bool {
- nameI, okI := entities[i].Attributes["friendly_name"].(string)
- nameJ, okJ := entities[j].Attributes["friendly_name"].(string)
- if !okI {
- nameI = entities[i].EntityID
- }
- if !okJ {
- nameJ = entities[j].EntityID
- }
- return nameI < nameJ
- })
- return entities, nil
- }
- func fetchHAEntities(config datatypes.JSON) ([]HAEntity, error) {
- var haConfig HAConfig
- b, err := config.MarshalJSON()
- if err != nil {
- return nil, fmt.Errorf("config error: %v", err)
- }
- if err := json.Unmarshal(b, &haConfig); err != nil {
- return nil, fmt.Errorf("invalid configuration format: %v", err)
- }
- if haConfig.URL == "" || haConfig.Token == "" {
- return nil, fmt.Errorf("URL and Token are required")
- }
- client := &http.Client{Timeout: 10 * time.Second}
- url := haConfig.URL
- // Robust URL handling
- url = strings.TrimSuffix(url, "/")
- url = strings.TrimSuffix(url, "/api")
- // Try Template API first to get device info
- // Using namespace to avoid list.append security restriction
- template := `
- {% set ns = namespace(result=[]) %}
- {% for state in states %}
- {% set name = state.attributes.friendly_name %}
- {% if name is not defined or name is none %}
- {% set name = state.entity_id %}
- {% endif %}
- {% set d = device_id(state.entity_id) %}
- {% if d %}
- {% set d_name = device_attr(d, 'name_by_user') or device_attr(d, 'name') or 'Unknown' %}
- {% set entry = {
- "id": state.entity_id,
- "s": state.state,
- "n": name,
- "did": d,
- "dn": d_name
- } %}
- {% set ns.result = ns.result + [entry] %}
- {% else %}
- {% set entry = {
- "id": state.entity_id,
- "s": state.state,
- "n": name,
- "did": "",
- "dn": ""
- } %}
- {% set ns.result = ns.result + [entry] %}
- {% endif %}
- {% endfor %}
- {{ ns.result | to_json }}
- `
- // Clean up newlines/spaces for template req? Not strictly needed for JSON but good practice
- // Actually JSON marshalling handles it.
-
- reqBody, _ := json.Marshal(HATemplateReq{Template: template})
- req, err := http.NewRequest("POST", url+"/api/template", bytes.NewBuffer(reqBody))
- if err == nil {
- req.Header.Set("Authorization", "Bearer "+haConfig.Token)
- req.Header.Set("Content-Type", "application/json")
-
- resp, err := client.Do(req)
- if err == nil && resp.StatusCode == 200 {
- defer resp.Body.Close()
- // Parse template result
- // HA returns string body which IS the rendered template (JSON)
- // But careful: sometimes it's plain text.
- // "to_json" filter ensures it's JSON.
-
- var tmplResults []HATemplateResult
- if err := json.NewDecoder(resp.Body).Decode(&tmplResults); err == nil {
- // Convert to HAEntity
- entities := make([]HAEntity, len(tmplResults))
- for i, r := range tmplResults {
- entities[i] = HAEntity{
- EntityID: r.ID,
- State: r.State,
- Attributes: map[string]interface{}{"friendly_name": r.Name}, // Simplified attributes
- DeviceID: r.DID,
- DeviceName: r.DName,
- }
- }
-
- // Sort by friendly_name
- sort.Slice(entities, func(i, j int) bool {
- nameI, okI := entities[i].Attributes["friendly_name"].(string)
- nameJ, okJ := entities[j].Attributes["friendly_name"].(string)
- if !okI {
- nameI = entities[i].EntityID
- }
- if !okJ {
- nameJ = entities[j].EntityID
- }
- return nameI < nameJ
- })
- return entities, nil
- }
- // If decode failed, fallthrough to legacy method
- }
- }
- // Fallback to /api/states
- req, err = http.NewRequest("GET", url+"/api/states", nil)
- if err != nil {
- return nil, fmt.Errorf("failed to create request: %v", err)
- }
- req.Header.Set("Authorization", "Bearer "+haConfig.Token)
- req.Header.Set("Content-Type", "application/json")
- resp, err := client.Do(req)
- if err != nil {
- return nil, fmt.Errorf("connection failed: %v", err)
- }
- defer resp.Body.Close()
- if resp.StatusCode != 200 {
- return nil, fmt.Errorf("Home Assistant returned status: %s", resp.Status)
- }
- var entities []HAEntity
- if err := json.NewDecoder(resp.Body).Decode(&entities); err != nil {
- return nil, fmt.Errorf("failed to decode response: %v", err)
- }
- // Sort by friendly_name
- sort.Slice(entities, func(i, j int) bool {
- nameI, okI := entities[i].Attributes["friendly_name"].(string)
- nameJ, okJ := entities[j].Attributes["friendly_name"].(string)
- if !okI {
- nameI = entities[i].EntityID
- }
- if !okJ {
- nameJ = entities[j].EntityID
- }
- return nameI < nameJ
- })
- return entities, nil
- }
- func testHAConnection(config datatypes.JSON) (bool, string) {
- var haConfig HAConfig
- b, err := config.MarshalJSON()
- if err != nil {
- return false, "Config error"
- }
- if err := json.Unmarshal(b, &haConfig); err != nil {
- return false, "Invalid configuration format"
- }
- if haConfig.URL == "" || haConfig.Token == "" {
- return false, "URL and Token are required"
- }
- client := &http.Client{Timeout: 5 * time.Second}
- // Removing trailing slash if present to avoid double slash
- url := haConfig.URL
- url = strings.TrimSuffix(url, "/")
- url = strings.TrimSuffix(url, "/api")
-
- req, err := http.NewRequest("GET", url+"/api/", nil)
- if err != nil {
- return false, "Failed to create request: " + err.Error()
- }
- req.Header.Set("Authorization", "Bearer "+haConfig.Token)
- req.Header.Set("Content-Type", "application/json")
- resp, err := client.Do(req)
- if err != nil {
- return false, "Connection failed: " + err.Error()
- }
- defer resp.Body.Close()
- if resp.StatusCode == 200 {
- return true, "Success"
- }
- return false, "Home Assistant returned status: " + resp.Status
- }
- func GetSources(c *gin.Context) {
- var sources []models.IntegrationSource
- if err := models.DB.Find(&sources).Error; err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
- c.JSON(http.StatusOK, sources)
- }
- func CreateSource(c *gin.Context) {
- var source models.IntegrationSource
- if err := c.ShouldBindJSON(&source); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
- // DEBUG LOG
- fmt.Printf("DEBUG: CreateSource received: %+v, Status: %s\n", source, source.Status)
- if err := models.DB.Create(&source).Error; err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
- c.JSON(http.StatusCreated, source)
- }
- func UpdateSource(c *gin.Context) {
- id := c.Param("id")
- var source models.IntegrationSource
- if err := models.DB.First(&source, "id = ?", id).Error; err != nil {
- c.JSON(http.StatusNotFound, gin.H{"error": "Source not found"})
- return
- }
- if err := c.ShouldBindJSON(&source); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
- // DEBUG LOG
- fmt.Printf("DEBUG: UpdateSource received: %+v, Status: %s\n", source, source.Status)
- models.DB.Save(&source)
- c.JSON(http.StatusOK, source)
- }
- func DeleteSource(c *gin.Context) {
- id := c.Param("id")
- if err := models.DB.Delete(&models.IntegrationSource{}, "id = ?", id).Error; err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
- c.JSON(http.StatusOK, gin.H{"message": "Source deleted"})
- }
- // TestSourceConnection 测试连接
- func TestSourceConnection(c *gin.Context) {
- var source models.IntegrationSource
- // 允许直接传参测试,或者传 ID 测试已有
- if err := c.ShouldBindJSON(&source); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
- var success bool
- var msg string
- switch source.DriverType {
- case "HOME_ASSISTANT":
- success, msg = testHAConnection(source.Config)
- default:
- // Mock others for now
- success = true
- msg = "Connection simulated (driver not implemented)"
- }
- if success {
- // If the source has an ID, update its status in DB
- if source.ID != uuid.Nil {
- models.DB.Model(&models.IntegrationSource{}).Where("id = ?", source.ID).Update("status", "ONLINE")
- }
- c.JSON(http.StatusOK, gin.H{"success": true, "message": "Connection successful"})
- } else {
- if source.ID != uuid.Nil {
- models.DB.Model(&models.IntegrationSource{}).Where("id = ?", source.ID).Update("status", "OFFLINE")
- }
- c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": msg})
- }
- }
- // SyncSource 同步数据
- func SyncSource(c *gin.Context) {
- id := c.Param("id")
- // TODO: 触发异步任务同步设备
- c.JSON(http.StatusOK, gin.H{"message": "Sync started for source " + id})
- }
- // GetSourceDevices 获取设备列表
- func GetSourceDevices(c *gin.Context) {
- id := c.Param("id")
- var source models.IntegrationSource
- if err := models.DB.First(&source, "id = ?", id).Error; err != nil {
- c.JSON(http.StatusNotFound, gin.H{"error": "Source not found"})
- return
- }
- if source.DriverType != "HOME_ASSISTANT" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "Only Home Assistant sources are supported"})
- return
- }
- devices, err := fetchHADevices(source.Config)
- if err != nil {
- fmt.Printf("DEBUG: GetSourceDevices error: %v\n", err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
- c.JSON(http.StatusOK, devices)
- }
- // GetSourceDeviceEntities 获取指定设备的实体列表
- func GetSourceDeviceEntities(c *gin.Context) {
- id := c.Param("id")
- deviceID := c.Param("deviceId")
-
- var source models.IntegrationSource
- if err := models.DB.First(&source, "id = ?", id).Error; err != nil {
- c.JSON(http.StatusNotFound, gin.H{"error": "Source not found"})
- return
- }
- if source.DriverType != "HOME_ASSISTANT" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "Only Home Assistant sources are supported"})
- return
- }
- entities, err := fetchHAEntitiesByDevice(source.Config, deviceID)
- if err != nil {
- fmt.Printf("DEBUG: GetSourceDeviceEntities error: %v\n", err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
- c.JSON(http.StatusOK, entities)
- }
- // GetSourceCandidates 获取数据源候选设备列表 (Deprecated or kept for backward compat)
- func GetSourceCandidates(c *gin.Context) {
- id := c.Param("id")
- var source models.IntegrationSource
- if err := models.DB.First(&source, "id = ?", id).Error; err != nil {
- c.JSON(http.StatusNotFound, gin.H{"error": "Source not found"})
- return
- }
- if source.DriverType != "HOME_ASSISTANT" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "Only Home Assistant sources are supported for candidate fetching currently"})
- return
- }
- entities, err := fetchHAEntities(source.Config)
- if err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
- c.JSON(http.StatusOK, entities)
- }
- // --- Device Controllers ---
- func GetDevices(c *gin.Context) {
- var devices []models.Device
- // Support filtering by location_id or source_id if provided
- locationID := c.Query("location_id")
- sourceID := c.Query("source_id")
-
- query := models.DB
- if locationID != "" {
- query = query.Where("location_id = ?", locationID)
- }
- if sourceID != "" {
- query = query.Where("source_id = ?", sourceID)
- }
-
- if err := query.Find(&devices).Error; err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
- c.JSON(http.StatusOK, devices)
- }
- func CreateDevice(c *gin.Context) {
- var device models.Device
- if err := c.ShouldBindJSON(&device); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
- if err := models.DB.Create(&device).Error; err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
- c.JSON(http.StatusCreated, device)
- }
- func UpdateDevice(c *gin.Context) {
- id := c.Param("id")
- var device models.Device
- if err := models.DB.First(&device, "id = ?", id).Error; err != nil {
- c.JSON(http.StatusNotFound, gin.H{"error": "Device not found"})
- return
- }
- if err := c.ShouldBindJSON(&device); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
- models.DB.Save(&device)
- c.JSON(http.StatusOK, device)
- }
- func DeleteDevice(c *gin.Context) {
- id := c.Param("id")
- if err := models.DB.Delete(&models.Device{}, "id = ?", id).Error; err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
- c.JSON(http.StatusOK, gin.H{"message": "Device deleted"})
- }
- // --- Location Controllers ---
- func GetLocations(c *gin.Context) {
- var locations []models.SysLocation
- if err := models.DB.Find(&locations).Error; err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
- c.JSON(http.StatusOK, locations)
- }
- func CreateLocation(c *gin.Context) {
- var location models.SysLocation
- if err := c.ShouldBindJSON(&location); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
- if err := models.DB.Create(&location).Error; err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
- c.JSON(http.StatusCreated, location)
- }
- func UpdateLocation(c *gin.Context) {
- id := c.Param("id")
- var location models.SysLocation
- if err := models.DB.First(&location, "id = ?", id).Error; err != nil {
- c.JSON(http.StatusNotFound, gin.H{"error": "Location not found"})
- return
- }
- if err := c.ShouldBindJSON(&location); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
- models.DB.Save(&location)
- c.JSON(http.StatusOK, location)
- }
- func DeleteLocation(c *gin.Context) {
- id := c.Param("id")
- if err := models.DB.Delete(&models.SysLocation{}, "id = ?", id).Error; err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
- c.JSON(http.StatusOK, gin.H{"message": "Location deleted"})
- }
- // GetDeviceHistory fetches historical data for devices
- func GetDeviceHistory(c *gin.Context) {
- // Parse Query Params
- // device_ids (comma separated)
- // metric (default power)
- // start, end (timestamps or ISO strings)
- // interval (e.g. 1m, 1h)
- deviceIDsStr := c.Query("device_ids")
- if deviceIDsStr == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "device_ids is required"})
- return
- }
- deviceIDs := strings.Split(deviceIDsStr, ",")
- metric := c.Query("metric")
- if metric == "" {
- metric = "power"
- }
- startStr := c.Query("start")
- endStr := c.Query("end")
- interval := c.Query("interval")
- // Default time range: last 24h
- end := time.Now()
- start := end.Add(-24 * time.Hour)
- if startStr != "" {
- if t, err := time.Parse(time.RFC3339, startStr); err == nil {
- start = t
- } else if t, err := time.Parse("2006-01-02 15:04:05", startStr); err == nil {
- start = t
- }
- }
- if endStr != "" {
- if t, err := time.Parse(time.RFC3339, endStr); err == nil {
- end = t
- } else if t, err := time.Parse("2006-01-02 15:04:05", endStr); err == nil {
- end = t
- }
- }
- data, err := db.GetReadings(deviceIDs, metric, start, end, interval)
- if err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
- c.JSON(http.StatusOK, data)
- }
|