resource_controller.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749
  1. package controllers
  2. import (
  3. "fmt"
  4. "ems-backend/db"
  5. "ems-backend/models"
  6. "encoding/json"
  7. "net/http"
  8. "sort"
  9. "time"
  10. "github.com/gin-gonic/gin"
  11. "github.com/google/uuid"
  12. "gorm.io/datatypes"
  13. "bytes"
  14. "io"
  15. "strings"
  16. )
  17. // --- Integration Source Controllers ---
  18. type HAConfig struct {
  19. URL string `json:"url"`
  20. Token string `json:"token"`
  21. }
  22. type HAEntity struct {
  23. EntityID string `json:"entity_id"`
  24. State string `json:"state"`
  25. Attributes map[string]interface{} `json:"attributes"`
  26. LastChanged time.Time `json:"last_changed"`
  27. LastUpdated time.Time `json:"last_updated"`
  28. DeviceID string `json:"device_id"` // Augmented field
  29. DeviceName string `json:"device_name"` // Augmented field
  30. }
  31. // HA Template Request
  32. type HATemplateReq struct {
  33. Template string `json:"template"`
  34. }
  35. // Struct for template result parsing
  36. type HATemplateResult struct {
  37. ID string `json:"id"`
  38. State string `json:"s"`
  39. Name string `json:"n"`
  40. DID string `json:"did"`
  41. DName string `json:"dn"`
  42. }
  43. // HADevice represents a Home Assistant Device
  44. type HADevice struct {
  45. ID string `json:"id"`
  46. Name string `json:"name"`
  47. Model string `json:"model"`
  48. Manufacturer string `json:"manufacturer"`
  49. }
  50. func fetchHADevices(config datatypes.JSON) ([]HADevice, error) {
  51. var haConfig HAConfig
  52. b, err := config.MarshalJSON()
  53. if err != nil {
  54. return nil, fmt.Errorf("config error: %v", err)
  55. }
  56. if err := json.Unmarshal(b, &haConfig); err != nil {
  57. return nil, fmt.Errorf("invalid configuration format: %v", err)
  58. }
  59. if haConfig.URL == "" || haConfig.Token == "" {
  60. return nil, fmt.Errorf("URL and Token are required")
  61. }
  62. client := &http.Client{Timeout: 10 * time.Second}
  63. url := haConfig.URL
  64. // Robust URL handling: remove trailing slash and /api suffix
  65. url = strings.TrimSuffix(url, "/")
  66. url = strings.TrimSuffix(url, "/api")
  67. // Use Template API to get devices efficiently
  68. // Simplified template avoiding list.append due to sandbox restrictions
  69. template := `
  70. {% set ns = namespace(result=[], devs=[]) %}
  71. {% for state in states %}
  72. {% set d = device_id(state.entity_id) %}
  73. {% if d and d not in ns.devs %}
  74. {% set ns.devs = ns.devs + [d] %}
  75. {% set name = device_attr(d, 'name_by_user') %}
  76. {% if not name %}
  77. {% set name = device_attr(d, 'name') %}
  78. {% endif %}
  79. {% if not name %}
  80. {% set name = 'Unknown' %}
  81. {% endif %}
  82. {% set entry = {
  83. "id": d,
  84. "name": name,
  85. "model": device_attr(d, 'model') or "",
  86. "manufacturer": device_attr(d, 'manufacturer') or ""
  87. } %}
  88. {% set ns.result = ns.result + [entry] %}
  89. {% endif %}
  90. {% endfor %}
  91. {{ ns.result | to_json }}
  92. `
  93. reqBody, _ := json.Marshal(HATemplateReq{Template: template})
  94. req, err := http.NewRequest("POST", url+"/api/template", bytes.NewBuffer(reqBody))
  95. if err != nil {
  96. return nil, fmt.Errorf("failed to create request: %v", err)
  97. }
  98. req.Header.Set("Authorization", "Bearer "+haConfig.Token)
  99. req.Header.Set("Content-Type", "application/json")
  100. resp, err := client.Do(req)
  101. if err != nil {
  102. return nil, fmt.Errorf("connection failed: %v", err)
  103. }
  104. defer resp.Body.Close()
  105. // Read body for better error reporting
  106. bodyBytes, err := io.ReadAll(resp.Body)
  107. if err != nil {
  108. return nil, fmt.Errorf("failed to read response body: %v", err)
  109. }
  110. if resp.StatusCode != 200 {
  111. fmt.Printf("DEBUG: HA Status Error: %s, Body: %s\n", resp.Status, string(bodyBytes))
  112. return nil, fmt.Errorf("Home Assistant returned status: %s. Body: %s", resp.Status, string(bodyBytes))
  113. }
  114. var devices []HADevice
  115. if err := json.Unmarshal(bodyBytes, &devices); err != nil {
  116. // Try to see if it's because empty result or format
  117. fmt.Printf("DEBUG: Failed to decode HA response: %s\nError: %v\n", string(bodyBytes), err)
  118. return nil, fmt.Errorf("failed to decode response: %v. Body: %s", err, string(bodyBytes))
  119. }
  120. fmt.Printf("DEBUG: Successfully fetched %d devices\n", len(devices))
  121. return devices, nil
  122. }
  123. func fetchHAEntitiesByDevice(config datatypes.JSON, deviceID string) ([]HAEntity, error) {
  124. var haConfig HAConfig
  125. b, err := config.MarshalJSON()
  126. if err != nil {
  127. return nil, fmt.Errorf("config error: %v", err)
  128. }
  129. if err := json.Unmarshal(b, &haConfig); err != nil {
  130. return nil, fmt.Errorf("invalid configuration format: %v", err)
  131. }
  132. if haConfig.URL == "" || haConfig.Token == "" {
  133. return nil, fmt.Errorf("URL and Token are required")
  134. }
  135. client := &http.Client{Timeout: 10 * time.Second}
  136. url := haConfig.URL
  137. // Robust URL handling
  138. url = strings.TrimSuffix(url, "/")
  139. url = strings.TrimSuffix(url, "/api")
  140. // Template to fetch entities for a specific device
  141. // Using strings.Replace to avoid fmt.Sprintf interpreting Jinja2 tags {% as format specifiers
  142. rawTemplate := `
  143. {% set ns = namespace(result=[]) %}
  144. {% set device_entities = device_entities('__DEVICE_ID__') %}
  145. {% for entity_id in device_entities %}
  146. {% set state = states[entity_id] %}
  147. {% if state %}
  148. {% set name = state.attributes.friendly_name %}
  149. {% if name is not defined or name is none %}
  150. {% set name = entity_id %}
  151. {% endif %}
  152. {% set entry = {
  153. "id": entity_id,
  154. "s": state.state,
  155. "n": name,
  156. "did": '__DEVICE_ID__',
  157. "dn": ''
  158. } %}
  159. {% set ns.result = ns.result + [entry] %}
  160. {% endif %}
  161. {% endfor %}
  162. {{ ns.result | to_json }}
  163. `
  164. template := strings.ReplaceAll(rawTemplate, "__DEVICE_ID__", deviceID)
  165. reqBody, _ := json.Marshal(HATemplateReq{Template: template})
  166. req, err := http.NewRequest("POST", url+"/api/template", bytes.NewBuffer(reqBody))
  167. if err != nil {
  168. return nil, fmt.Errorf("failed to create request: %v", err)
  169. }
  170. req.Header.Set("Authorization", "Bearer "+haConfig.Token)
  171. req.Header.Set("Content-Type", "application/json")
  172. resp, err := client.Do(req)
  173. if err != nil {
  174. return nil, fmt.Errorf("connection failed: %v", err)
  175. }
  176. defer resp.Body.Close()
  177. // Read body for better error reporting
  178. bodyBytes, err := io.ReadAll(resp.Body)
  179. if err != nil {
  180. return nil, fmt.Errorf("failed to read response body: %v", err)
  181. }
  182. if resp.StatusCode != 200 {
  183. fmt.Printf("DEBUG: HA Status Error (Entities): %s, Body: %s\n", resp.Status, string(bodyBytes))
  184. return nil, fmt.Errorf("Home Assistant returned status: %s. Body: %s", resp.Status, string(bodyBytes))
  185. }
  186. var tmplResults []HATemplateResult
  187. if err := json.Unmarshal(bodyBytes, &tmplResults); err != nil {
  188. fmt.Printf("DEBUG: Failed to decode HA response (Entities): %s\nError: %v\n", string(bodyBytes), err)
  189. return nil, fmt.Errorf("failed to decode response: %v. Body: %s", err, string(bodyBytes))
  190. }
  191. fmt.Printf("DEBUG: Successfully fetched %d entities for device %s\n", len(tmplResults), deviceID)
  192. entities := make([]HAEntity, len(tmplResults))
  193. for i, r := range tmplResults {
  194. entities[i] = HAEntity{
  195. EntityID: r.ID,
  196. State: r.State,
  197. Attributes: map[string]interface{}{"friendly_name": r.Name},
  198. DeviceID: r.DID,
  199. DeviceName: r.DName,
  200. }
  201. }
  202. // Sort by friendly_name
  203. sort.Slice(entities, func(i, j int) bool {
  204. nameI, okI := entities[i].Attributes["friendly_name"].(string)
  205. nameJ, okJ := entities[j].Attributes["friendly_name"].(string)
  206. if !okI {
  207. nameI = entities[i].EntityID
  208. }
  209. if !okJ {
  210. nameJ = entities[j].EntityID
  211. }
  212. return nameI < nameJ
  213. })
  214. return entities, nil
  215. }
  216. func fetchHAEntities(config datatypes.JSON) ([]HAEntity, error) {
  217. var haConfig HAConfig
  218. b, err := config.MarshalJSON()
  219. if err != nil {
  220. return nil, fmt.Errorf("config error: %v", err)
  221. }
  222. if err := json.Unmarshal(b, &haConfig); err != nil {
  223. return nil, fmt.Errorf("invalid configuration format: %v", err)
  224. }
  225. if haConfig.URL == "" || haConfig.Token == "" {
  226. return nil, fmt.Errorf("URL and Token are required")
  227. }
  228. client := &http.Client{Timeout: 10 * time.Second}
  229. url := haConfig.URL
  230. // Robust URL handling
  231. url = strings.TrimSuffix(url, "/")
  232. url = strings.TrimSuffix(url, "/api")
  233. // Try Template API first to get device info
  234. // Using namespace to avoid list.append security restriction
  235. template := `
  236. {% set ns = namespace(result=[]) %}
  237. {% for state in states %}
  238. {% set name = state.attributes.friendly_name %}
  239. {% if name is not defined or name is none %}
  240. {% set name = state.entity_id %}
  241. {% endif %}
  242. {% set d = device_id(state.entity_id) %}
  243. {% if d %}
  244. {% set d_name = device_attr(d, 'name_by_user') or device_attr(d, 'name') or 'Unknown' %}
  245. {% set entry = {
  246. "id": state.entity_id,
  247. "s": state.state,
  248. "n": name,
  249. "did": d,
  250. "dn": d_name
  251. } %}
  252. {% set ns.result = ns.result + [entry] %}
  253. {% else %}
  254. {% set entry = {
  255. "id": state.entity_id,
  256. "s": state.state,
  257. "n": name,
  258. "did": "",
  259. "dn": ""
  260. } %}
  261. {% set ns.result = ns.result + [entry] %}
  262. {% endif %}
  263. {% endfor %}
  264. {{ ns.result | to_json }}
  265. `
  266. // Clean up newlines/spaces for template req? Not strictly needed for JSON but good practice
  267. // Actually JSON marshalling handles it.
  268. reqBody, _ := json.Marshal(HATemplateReq{Template: template})
  269. req, err := http.NewRequest("POST", url+"/api/template", bytes.NewBuffer(reqBody))
  270. if err == nil {
  271. req.Header.Set("Authorization", "Bearer "+haConfig.Token)
  272. req.Header.Set("Content-Type", "application/json")
  273. resp, err := client.Do(req)
  274. if err == nil && resp.StatusCode == 200 {
  275. defer resp.Body.Close()
  276. // Parse template result
  277. // HA returns string body which IS the rendered template (JSON)
  278. // But careful: sometimes it's plain text.
  279. // "to_json" filter ensures it's JSON.
  280. var tmplResults []HATemplateResult
  281. if err := json.NewDecoder(resp.Body).Decode(&tmplResults); err == nil {
  282. // Convert to HAEntity
  283. entities := make([]HAEntity, len(tmplResults))
  284. for i, r := range tmplResults {
  285. entities[i] = HAEntity{
  286. EntityID: r.ID,
  287. State: r.State,
  288. Attributes: map[string]interface{}{"friendly_name": r.Name}, // Simplified attributes
  289. DeviceID: r.DID,
  290. DeviceName: r.DName,
  291. }
  292. }
  293. // Sort by friendly_name
  294. sort.Slice(entities, func(i, j int) bool {
  295. nameI, okI := entities[i].Attributes["friendly_name"].(string)
  296. nameJ, okJ := entities[j].Attributes["friendly_name"].(string)
  297. if !okI {
  298. nameI = entities[i].EntityID
  299. }
  300. if !okJ {
  301. nameJ = entities[j].EntityID
  302. }
  303. return nameI < nameJ
  304. })
  305. return entities, nil
  306. }
  307. // If decode failed, fallthrough to legacy method
  308. }
  309. }
  310. // Fallback to /api/states
  311. req, err = http.NewRequest("GET", url+"/api/states", nil)
  312. if err != nil {
  313. return nil, fmt.Errorf("failed to create request: %v", err)
  314. }
  315. req.Header.Set("Authorization", "Bearer "+haConfig.Token)
  316. req.Header.Set("Content-Type", "application/json")
  317. resp, err := client.Do(req)
  318. if err != nil {
  319. return nil, fmt.Errorf("connection failed: %v", err)
  320. }
  321. defer resp.Body.Close()
  322. if resp.StatusCode != 200 {
  323. return nil, fmt.Errorf("Home Assistant returned status: %s", resp.Status)
  324. }
  325. var entities []HAEntity
  326. if err := json.NewDecoder(resp.Body).Decode(&entities); err != nil {
  327. return nil, fmt.Errorf("failed to decode response: %v", err)
  328. }
  329. // Sort by friendly_name
  330. sort.Slice(entities, func(i, j int) bool {
  331. nameI, okI := entities[i].Attributes["friendly_name"].(string)
  332. nameJ, okJ := entities[j].Attributes["friendly_name"].(string)
  333. if !okI {
  334. nameI = entities[i].EntityID
  335. }
  336. if !okJ {
  337. nameJ = entities[j].EntityID
  338. }
  339. return nameI < nameJ
  340. })
  341. return entities, nil
  342. }
  343. func testHAConnection(config datatypes.JSON) (bool, string) {
  344. var haConfig HAConfig
  345. b, err := config.MarshalJSON()
  346. if err != nil {
  347. return false, "Config error"
  348. }
  349. if err := json.Unmarshal(b, &haConfig); err != nil {
  350. return false, "Invalid configuration format"
  351. }
  352. if haConfig.URL == "" || haConfig.Token == "" {
  353. return false, "URL and Token are required"
  354. }
  355. client := &http.Client{Timeout: 5 * time.Second}
  356. // Removing trailing slash if present to avoid double slash
  357. url := haConfig.URL
  358. url = strings.TrimSuffix(url, "/")
  359. url = strings.TrimSuffix(url, "/api")
  360. req, err := http.NewRequest("GET", url+"/api/", nil)
  361. if err != nil {
  362. return false, "Failed to create request: " + err.Error()
  363. }
  364. req.Header.Set("Authorization", "Bearer "+haConfig.Token)
  365. req.Header.Set("Content-Type", "application/json")
  366. resp, err := client.Do(req)
  367. if err != nil {
  368. return false, "Connection failed: " + err.Error()
  369. }
  370. defer resp.Body.Close()
  371. if resp.StatusCode == 200 {
  372. return true, "Success"
  373. }
  374. return false, "Home Assistant returned status: " + resp.Status
  375. }
  376. func GetSources(c *gin.Context) {
  377. var sources []models.IntegrationSource
  378. if err := models.DB.Find(&sources).Error; err != nil {
  379. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  380. return
  381. }
  382. c.JSON(http.StatusOK, sources)
  383. }
  384. func CreateSource(c *gin.Context) {
  385. var source models.IntegrationSource
  386. if err := c.ShouldBindJSON(&source); err != nil {
  387. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  388. return
  389. }
  390. // DEBUG LOG
  391. fmt.Printf("DEBUG: CreateSource received: %+v, Status: %s\n", source, source.Status)
  392. if err := models.DB.Create(&source).Error; err != nil {
  393. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  394. return
  395. }
  396. c.JSON(http.StatusCreated, source)
  397. }
  398. func UpdateSource(c *gin.Context) {
  399. id := c.Param("id")
  400. var source models.IntegrationSource
  401. if err := models.DB.First(&source, "id = ?", id).Error; err != nil {
  402. c.JSON(http.StatusNotFound, gin.H{"error": "Source not found"})
  403. return
  404. }
  405. if err := c.ShouldBindJSON(&source); err != nil {
  406. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  407. return
  408. }
  409. // DEBUG LOG
  410. fmt.Printf("DEBUG: UpdateSource received: %+v, Status: %s\n", source, source.Status)
  411. models.DB.Save(&source)
  412. c.JSON(http.StatusOK, source)
  413. }
  414. func DeleteSource(c *gin.Context) {
  415. id := c.Param("id")
  416. if err := models.DB.Delete(&models.IntegrationSource{}, "id = ?", id).Error; err != nil {
  417. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  418. return
  419. }
  420. c.JSON(http.StatusOK, gin.H{"message": "Source deleted"})
  421. }
  422. // TestSourceConnection 测试连接
  423. func TestSourceConnection(c *gin.Context) {
  424. var source models.IntegrationSource
  425. // 允许直接传参测试,或者传 ID 测试已有
  426. if err := c.ShouldBindJSON(&source); err != nil {
  427. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  428. return
  429. }
  430. var success bool
  431. var msg string
  432. switch source.DriverType {
  433. case "HOME_ASSISTANT":
  434. success, msg = testHAConnection(source.Config)
  435. default:
  436. // Mock others for now
  437. success = true
  438. msg = "Connection simulated (driver not implemented)"
  439. }
  440. if success {
  441. // If the source has an ID, update its status in DB
  442. if source.ID != uuid.Nil {
  443. models.DB.Model(&models.IntegrationSource{}).Where("id = ?", source.ID).Update("status", "ONLINE")
  444. }
  445. c.JSON(http.StatusOK, gin.H{"success": true, "message": "Connection successful"})
  446. } else {
  447. if source.ID != uuid.Nil {
  448. models.DB.Model(&models.IntegrationSource{}).Where("id = ?", source.ID).Update("status", "OFFLINE")
  449. }
  450. c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": msg})
  451. }
  452. }
  453. // SyncSource 同步数据
  454. func SyncSource(c *gin.Context) {
  455. id := c.Param("id")
  456. // TODO: 触发异步任务同步设备
  457. c.JSON(http.StatusOK, gin.H{"message": "Sync started for source " + id})
  458. }
  459. // GetSourceDevices 获取设备列表
  460. func GetSourceDevices(c *gin.Context) {
  461. id := c.Param("id")
  462. var source models.IntegrationSource
  463. if err := models.DB.First(&source, "id = ?", id).Error; err != nil {
  464. c.JSON(http.StatusNotFound, gin.H{"error": "Source not found"})
  465. return
  466. }
  467. if source.DriverType != "HOME_ASSISTANT" {
  468. c.JSON(http.StatusBadRequest, gin.H{"error": "Only Home Assistant sources are supported"})
  469. return
  470. }
  471. devices, err := fetchHADevices(source.Config)
  472. if err != nil {
  473. fmt.Printf("DEBUG: GetSourceDevices error: %v\n", err)
  474. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  475. return
  476. }
  477. c.JSON(http.StatusOK, devices)
  478. }
  479. // GetSourceDeviceEntities 获取指定设备的实体列表
  480. func GetSourceDeviceEntities(c *gin.Context) {
  481. id := c.Param("id")
  482. deviceID := c.Param("deviceId")
  483. var source models.IntegrationSource
  484. if err := models.DB.First(&source, "id = ?", id).Error; err != nil {
  485. c.JSON(http.StatusNotFound, gin.H{"error": "Source not found"})
  486. return
  487. }
  488. if source.DriverType != "HOME_ASSISTANT" {
  489. c.JSON(http.StatusBadRequest, gin.H{"error": "Only Home Assistant sources are supported"})
  490. return
  491. }
  492. entities, err := fetchHAEntitiesByDevice(source.Config, deviceID)
  493. if err != nil {
  494. fmt.Printf("DEBUG: GetSourceDeviceEntities error: %v\n", err)
  495. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  496. return
  497. }
  498. c.JSON(http.StatusOK, entities)
  499. }
  500. // GetSourceCandidates 获取数据源候选设备列表 (Deprecated or kept for backward compat)
  501. func GetSourceCandidates(c *gin.Context) {
  502. id := c.Param("id")
  503. var source models.IntegrationSource
  504. if err := models.DB.First(&source, "id = ?", id).Error; err != nil {
  505. c.JSON(http.StatusNotFound, gin.H{"error": "Source not found"})
  506. return
  507. }
  508. if source.DriverType != "HOME_ASSISTANT" {
  509. c.JSON(http.StatusBadRequest, gin.H{"error": "Only Home Assistant sources are supported for candidate fetching currently"})
  510. return
  511. }
  512. entities, err := fetchHAEntities(source.Config)
  513. if err != nil {
  514. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  515. return
  516. }
  517. c.JSON(http.StatusOK, entities)
  518. }
  519. // --- Device Controllers ---
  520. func GetDevices(c *gin.Context) {
  521. var devices []models.Device
  522. // Support filtering by location_id or source_id if provided
  523. locationID := c.Query("location_id")
  524. sourceID := c.Query("source_id")
  525. query := models.DB
  526. if locationID != "" {
  527. query = query.Where("location_id = ?", locationID)
  528. }
  529. if sourceID != "" {
  530. query = query.Where("source_id = ?", sourceID)
  531. }
  532. if err := query.Find(&devices).Error; err != nil {
  533. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  534. return
  535. }
  536. c.JSON(http.StatusOK, devices)
  537. }
  538. func CreateDevice(c *gin.Context) {
  539. var device models.Device
  540. if err := c.ShouldBindJSON(&device); err != nil {
  541. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  542. return
  543. }
  544. if err := models.DB.Create(&device).Error; err != nil {
  545. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  546. return
  547. }
  548. c.JSON(http.StatusCreated, device)
  549. }
  550. func UpdateDevice(c *gin.Context) {
  551. id := c.Param("id")
  552. var device models.Device
  553. if err := models.DB.First(&device, "id = ?", id).Error; err != nil {
  554. c.JSON(http.StatusNotFound, gin.H{"error": "Device not found"})
  555. return
  556. }
  557. if err := c.ShouldBindJSON(&device); err != nil {
  558. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  559. return
  560. }
  561. models.DB.Save(&device)
  562. c.JSON(http.StatusOK, device)
  563. }
  564. func DeleteDevice(c *gin.Context) {
  565. id := c.Param("id")
  566. if err := models.DB.Delete(&models.Device{}, "id = ?", id).Error; err != nil {
  567. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  568. return
  569. }
  570. c.JSON(http.StatusOK, gin.H{"message": "Device deleted"})
  571. }
  572. // --- Location Controllers ---
  573. func GetLocations(c *gin.Context) {
  574. var locations []models.SysLocation
  575. if err := models.DB.Find(&locations).Error; err != nil {
  576. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  577. return
  578. }
  579. c.JSON(http.StatusOK, locations)
  580. }
  581. func CreateLocation(c *gin.Context) {
  582. var location models.SysLocation
  583. if err := c.ShouldBindJSON(&location); err != nil {
  584. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  585. return
  586. }
  587. if err := models.DB.Create(&location).Error; err != nil {
  588. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  589. return
  590. }
  591. c.JSON(http.StatusCreated, location)
  592. }
  593. func UpdateLocation(c *gin.Context) {
  594. id := c.Param("id")
  595. var location models.SysLocation
  596. if err := models.DB.First(&location, "id = ?", id).Error; err != nil {
  597. c.JSON(http.StatusNotFound, gin.H{"error": "Location not found"})
  598. return
  599. }
  600. if err := c.ShouldBindJSON(&location); err != nil {
  601. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  602. return
  603. }
  604. models.DB.Save(&location)
  605. c.JSON(http.StatusOK, location)
  606. }
  607. func DeleteLocation(c *gin.Context) {
  608. id := c.Param("id")
  609. if err := models.DB.Delete(&models.SysLocation{}, "id = ?", id).Error; err != nil {
  610. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  611. return
  612. }
  613. c.JSON(http.StatusOK, gin.H{"message": "Location deleted"})
  614. }
  615. // GetDeviceHistory fetches historical data for devices
  616. func GetDeviceHistory(c *gin.Context) {
  617. // Parse Query Params
  618. // device_ids (comma separated)
  619. // metric (default power)
  620. // start, end (timestamps or ISO strings)
  621. // interval (e.g. 1m, 1h)
  622. deviceIDsStr := c.Query("device_ids")
  623. if deviceIDsStr == "" {
  624. c.JSON(http.StatusBadRequest, gin.H{"error": "device_ids is required"})
  625. return
  626. }
  627. deviceIDs := strings.Split(deviceIDsStr, ",")
  628. metric := c.Query("metric")
  629. if metric == "" {
  630. metric = "power"
  631. }
  632. startStr := c.Query("start")
  633. endStr := c.Query("end")
  634. interval := c.Query("interval")
  635. // Default time range: last 24h
  636. end := time.Now()
  637. start := end.Add(-24 * time.Hour)
  638. if startStr != "" {
  639. if t, err := time.Parse(time.RFC3339, startStr); err == nil {
  640. start = t
  641. } else if t, err := time.Parse("2006-01-02 15:04:05", startStr); err == nil {
  642. start = t
  643. }
  644. }
  645. if endStr != "" {
  646. if t, err := time.Parse(time.RFC3339, endStr); err == nil {
  647. end = t
  648. } else if t, err := time.Parse("2006-01-02 15:04:05", endStr); err == nil {
  649. end = t
  650. }
  651. }
  652. data, err := db.GetReadings(deviceIDs, metric, start, end, interval)
  653. if err != nil {
  654. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  655. return
  656. }
  657. c.JSON(http.StatusOK, data)
  658. }