resource_controller.go 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888
  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. // CallSourceService 调用数据源服务 (Home Assistant Call Service)
  520. type ServiceCallReq struct {
  521. Domain string `json:"domain"`
  522. Service string `json:"service"`
  523. ServiceData map[string]interface{} `json:"service_data"`
  524. }
  525. func CallSourceService(c *gin.Context) {
  526. id := c.Param("id")
  527. var req ServiceCallReq
  528. if err := c.ShouldBindJSON(&req); err != nil {
  529. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  530. return
  531. }
  532. var source models.IntegrationSource
  533. if err := models.DB.First(&source, "id = ?", id).Error; err != nil {
  534. c.JSON(http.StatusNotFound, gin.H{"error": "Source not found"})
  535. return
  536. }
  537. if source.DriverType != "HOME_ASSISTANT" {
  538. c.JSON(http.StatusBadRequest, gin.H{"error": "Only Home Assistant sources are supported"})
  539. return
  540. }
  541. // Call HA API
  542. var haConfig HAConfig
  543. b, _ := source.Config.MarshalJSON()
  544. json.Unmarshal(b, &haConfig) // Error ignored as DB data should be valid
  545. if haConfig.URL == "" || haConfig.Token == "" {
  546. c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid source configuration"})
  547. return
  548. }
  549. client := &http.Client{Timeout: 10 * time.Second}
  550. url := haConfig.URL
  551. url = strings.TrimSuffix(url, "/")
  552. url = strings.TrimSuffix(url, "/api")
  553. // Target URL: /api/services/<domain>/<service>
  554. targetURL := fmt.Sprintf("%s/api/services/%s/%s", url, req.Domain, req.Service)
  555. reqBody, _ := json.Marshal(req.ServiceData)
  556. httpReq, err := http.NewRequest("POST", targetURL, bytes.NewBuffer(reqBody))
  557. if err != nil {
  558. c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create request: " + err.Error()})
  559. return
  560. }
  561. httpReq.Header.Set("Authorization", "Bearer "+haConfig.Token)
  562. httpReq.Header.Set("Content-Type", "application/json")
  563. resp, err := client.Do(httpReq)
  564. if err != nil {
  565. c.JSON(http.StatusInternalServerError, gin.H{"error": "Connection failed: " + err.Error()})
  566. return
  567. }
  568. defer resp.Body.Close()
  569. bodyBytes, _ := io.ReadAll(resp.Body)
  570. if resp.StatusCode != 200 && resp.StatusCode != 201 {
  571. c.JSON(http.StatusBadGateway, gin.H{"error": "HA Error", "details": string(bodyBytes)})
  572. return
  573. }
  574. // HA usually returns a list of state changes
  575. var result interface{}
  576. json.Unmarshal(bodyBytes, &result)
  577. c.JSON(http.StatusOK, result)
  578. }
  579. // --- Device Controllers ---
  580. func GetDevices(c *gin.Context) {
  581. var devices []models.Device
  582. // Support filtering by location_id or source_id if provided
  583. locationID := c.Query("location_id")
  584. sourceID := c.Query("source_id")
  585. query := models.DB
  586. if locationID != "" {
  587. query = query.Where("location_id = ?", locationID)
  588. }
  589. if sourceID != "" {
  590. query = query.Where("source_id = ?", sourceID)
  591. }
  592. if err := query.Find(&devices).Error; err != nil {
  593. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  594. return
  595. }
  596. c.JSON(http.StatusOK, devices)
  597. }
  598. func GetDevice(c *gin.Context) {
  599. id := c.Param("id")
  600. var device models.Device
  601. if err := models.DB.First(&device, "id = ?", id).Error; err != nil {
  602. c.JSON(http.StatusNotFound, gin.H{"error": "Device not found"})
  603. return
  604. }
  605. c.JSON(http.StatusOK, device)
  606. }
  607. func GetDeviceRealtime(c *gin.Context) {
  608. id := c.Param("id")
  609. // Fetch latest data from TDengine (Cleaned Data)
  610. data, err := db.GetLatestDeviceData(id)
  611. if err != nil {
  612. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  613. return
  614. }
  615. c.JSON(http.StatusOK, data)
  616. }
  617. func CreateDevice(c *gin.Context) {
  618. var device models.Device
  619. if err := c.ShouldBindJSON(&device); err != nil {
  620. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  621. return
  622. }
  623. if err := models.DB.Create(&device).Error; err != nil {
  624. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  625. return
  626. }
  627. c.JSON(http.StatusCreated, device)
  628. }
  629. func UpdateDevice(c *gin.Context) {
  630. id := c.Param("id")
  631. var device models.Device
  632. if err := models.DB.First(&device, "id = ?", id).Error; err != nil {
  633. c.JSON(http.StatusNotFound, gin.H{"error": "Device not found"})
  634. return
  635. }
  636. if err := c.ShouldBindJSON(&device); err != nil {
  637. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  638. return
  639. }
  640. models.DB.Save(&device)
  641. c.JSON(http.StatusOK, device)
  642. }
  643. func DeleteDevice(c *gin.Context) {
  644. id := c.Param("id")
  645. if err := models.DB.Delete(&models.Device{}, "id = ?", id).Error; err != nil {
  646. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  647. return
  648. }
  649. c.JSON(http.StatusOK, gin.H{"message": "Device deleted"})
  650. }
  651. // --- Location Controllers ---
  652. func GetLocations(c *gin.Context) {
  653. var locations []models.SysLocation
  654. if err := models.DB.Find(&locations).Error; err != nil {
  655. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  656. return
  657. }
  658. c.JSON(http.StatusOK, locations)
  659. }
  660. func CreateLocation(c *gin.Context) {
  661. var location models.SysLocation
  662. if err := c.ShouldBindJSON(&location); err != nil {
  663. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  664. return
  665. }
  666. if err := models.DB.Create(&location).Error; err != nil {
  667. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  668. return
  669. }
  670. c.JSON(http.StatusCreated, location)
  671. }
  672. func UpdateLocation(c *gin.Context) {
  673. id := c.Param("id")
  674. var location models.SysLocation
  675. if err := models.DB.First(&location, "id = ?", id).Error; err != nil {
  676. c.JSON(http.StatusNotFound, gin.H{"error": "Location not found"})
  677. return
  678. }
  679. if err := c.ShouldBindJSON(&location); err != nil {
  680. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  681. return
  682. }
  683. models.DB.Save(&location)
  684. c.JSON(http.StatusOK, location)
  685. }
  686. func DeleteLocation(c *gin.Context) {
  687. id := c.Param("id")
  688. if err := models.DB.Delete(&models.SysLocation{}, "id = ?", id).Error; err != nil {
  689. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  690. return
  691. }
  692. c.JSON(http.StatusOK, gin.H{"message": "Location deleted"})
  693. }
  694. // GetDeviceHistory fetches historical data for devices
  695. func GetDeviceHistory(c *gin.Context) {
  696. // Parse Query Params
  697. // device_ids (comma separated)
  698. // metric (default power)
  699. // start, end (timestamps or ISO strings)
  700. // interval (e.g. 1m, 1h)
  701. deviceIDsStr := c.Query("device_ids")
  702. if deviceIDsStr == "" {
  703. c.JSON(http.StatusBadRequest, gin.H{"error": "device_ids is required"})
  704. return
  705. }
  706. deviceIDs := strings.Split(deviceIDsStr, ",")
  707. metric := c.Query("metric")
  708. // if metric == "" {
  709. // metric = "power"
  710. // }
  711. startStr := c.Query("start")
  712. endStr := c.Query("end")
  713. interval := c.Query("interval")
  714. if interval == "" {
  715. interval = "raw"
  716. }
  717. // Default time range: last 24h
  718. end := time.Now()
  719. start := end.Add(-24 * time.Hour)
  720. if startStr != "" {
  721. if t, err := time.Parse(time.RFC3339, startStr); err == nil {
  722. start = t
  723. } else if t, err := time.ParseInLocation("2006-01-02 15:04:05", startStr, time.Local); err == nil {
  724. start = t
  725. }
  726. }
  727. if endStr != "" {
  728. if t, err := time.Parse(time.RFC3339, endStr); err == nil {
  729. end = t
  730. } else if t, err := time.ParseInLocation("2006-01-02 15:04:05", endStr, time.Local); err == nil {
  731. end = t
  732. }
  733. }
  734. data, err := db.GetReadings(deviceIDs, metric, start, end, interval)
  735. if err != nil {
  736. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  737. return
  738. }
  739. c.JSON(http.StatusOK, data)
  740. }
  741. // DeleteDeviceHistory deletes history records in a range
  742. func DeleteDeviceHistory(c *gin.Context) {
  743. // Query params: device_id, metric, start, end
  744. deviceID := c.Query("device_id")
  745. metric := c.Query("metric")
  746. startStr := c.Query("start")
  747. endStr := c.Query("end")
  748. if deviceID == "" || metric == "" || startStr == "" || endStr == "" {
  749. c.JSON(http.StatusBadRequest, gin.H{"error": "device_id, metric, start, and end are required"})
  750. return
  751. }
  752. var start, end time.Time
  753. var err error
  754. // Parse Start
  755. if start, err = time.ParseInLocation("2006-01-02 15:04:05", startStr, time.Local); err != nil {
  756. if start, err = time.Parse(time.RFC3339, startStr); err != nil {
  757. c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid start format"})
  758. return
  759. }
  760. }
  761. // Parse End
  762. if end, err = time.ParseInLocation("2006-01-02 15:04:05", endStr, time.Local); err != nil {
  763. if end, err = time.Parse(time.RFC3339, endStr); err != nil {
  764. c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid end format"})
  765. return
  766. }
  767. }
  768. if err := db.DeleteReadings(deviceID, metric, start, end); err != nil {
  769. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  770. return
  771. }
  772. c.JSON(http.StatusOK, gin.H{"message": "Readings deleted"})
  773. }