ha_client.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. package utils
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "fmt"
  6. "net/http"
  7. "sort"
  8. "strings"
  9. "time"
  10. "gorm.io/datatypes"
  11. )
  12. // Global HTTP Client for HA
  13. var haHttpClient = &http.Client{
  14. Timeout: 10 * time.Second,
  15. Transport: &http.Transport{
  16. MaxIdleConns: 100,
  17. MaxIdleConnsPerHost: 10,
  18. IdleConnTimeout: 90 * time.Second,
  19. },
  20. }
  21. type HAConfig struct {
  22. URL string `json:"url"`
  23. Token string `json:"token"`
  24. }
  25. type HAEntity struct {
  26. EntityID string `json:"entity_id"`
  27. State string `json:"state"`
  28. Attributes map[string]interface{} `json:"attributes"`
  29. LastChanged time.Time `json:"last_changed"`
  30. LastUpdated time.Time `json:"last_updated"`
  31. DeviceID string `json:"device_id"`
  32. DeviceName string `json:"device_name"`
  33. }
  34. type HATemplateReq struct {
  35. Template string `json:"template"`
  36. }
  37. type HATemplateResult struct {
  38. ID string `json:"id"`
  39. State string `json:"s"`
  40. Name string `json:"n"`
  41. DID string `json:"did"`
  42. DName string `json:"dn"`
  43. }
  44. // FetchHAEntitiesByDevice fetches entities for a specific device using HA Template API
  45. func FetchHAEntitiesByDevice(config datatypes.JSON, deviceID string) ([]HAEntity, error) {
  46. haConfig, err := parseHAConfig(config)
  47. if err != nil {
  48. return nil, err
  49. }
  50. url := normalizeURL(haConfig.URL)
  51. rawTemplate := `
  52. {% set ns = namespace(result=[]) %}
  53. {% set device_entities = device_entities('__DEVICE_ID__') %}
  54. {% for entity_id in device_entities %}
  55. {% set state = states[entity_id] %}
  56. {% if state %}
  57. {% set name = state.attributes.friendly_name %}
  58. {% if name is not defined or name is none %}
  59. {% set name = entity_id %}
  60. {% endif %}
  61. {% set entry = {
  62. "id": entity_id,
  63. "s": state.state,
  64. "n": name,
  65. "did": '__DEVICE_ID__',
  66. "dn": ''
  67. } %}
  68. {% set ns.result = ns.result + [entry] %}
  69. {% endif %}
  70. {% endfor %}
  71. {{ ns.result | to_json }}
  72. `
  73. template := strings.ReplaceAll(rawTemplate, "__DEVICE_ID__", deviceID)
  74. return executeTemplateQuery(haHttpClient, url, haConfig.Token, template)
  75. }
  76. // FetchEntityState fetches a single entity state
  77. func FetchEntityState(config datatypes.JSON, entityID string) (*HAEntity, error) {
  78. haConfig, err := parseHAConfig(config)
  79. if err != nil {
  80. return nil, err
  81. }
  82. url := normalizeURL(haConfig.URL)
  83. req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/states/%s", url, entityID), nil)
  84. if err != nil {
  85. return nil, err
  86. }
  87. req.Header.Set("Authorization", "Bearer "+haConfig.Token)
  88. req.Header.Set("Content-Type", "application/json")
  89. resp, err := haHttpClient.Do(req)
  90. if err != nil {
  91. return nil, err
  92. }
  93. defer resp.Body.Close()
  94. if resp.StatusCode != 200 {
  95. return nil, fmt.Errorf("HA returned status: %s", resp.Status)
  96. }
  97. var entity HAEntity
  98. if err := json.NewDecoder(resp.Body).Decode(&entity); err != nil {
  99. return nil, err
  100. }
  101. return &entity, nil
  102. }
  103. // BatchFetchStates fetches multiple entities in one go using states API
  104. func BatchFetchStates(config datatypes.JSON, entityIDs []string) (map[string]string, error) {
  105. if len(entityIDs) == 0 {
  106. return map[string]string{}, nil
  107. }
  108. haConfig, err := parseHAConfig(config)
  109. if err != nil {
  110. return nil, err
  111. }
  112. url := normalizeURL(haConfig.URL)
  113. // Direct State API fetch - more reliable than Template for large lists or weird IDs
  114. req, err := http.NewRequest("GET", url+"/api/states", nil)
  115. if err != nil {
  116. return nil, err
  117. }
  118. req.Header.Set("Authorization", "Bearer "+haConfig.Token)
  119. req.Header.Set("Content-Type", "application/json")
  120. fmt.Printf("DEBUG: Fetching all states from %s\n", url+"/api/states")
  121. resp, err := haHttpClient.Do(req)
  122. if err != nil {
  123. return nil, err
  124. }
  125. defer resp.Body.Close()
  126. if resp.StatusCode != 200 {
  127. return nil, fmt.Errorf("HA error: %s", resp.Status)
  128. }
  129. // Helper struct for decoding the list response
  130. type StateItem struct {
  131. EntityID string `json:"entity_id"`
  132. State string `json:"state"`
  133. }
  134. var allStates []StateItem
  135. if err := json.NewDecoder(resp.Body).Decode(&allStates); err != nil {
  136. return nil, fmt.Errorf("decode error: %v", err)
  137. }
  138. fmt.Printf("DEBUG: Got %d total states from HA\n", len(allStates))
  139. // Convert list to map for filtering
  140. resultMap := make(map[string]string)
  141. // Create a lookup set for requested IDs
  142. targetIDs := make(map[string]bool)
  143. for _, id := range entityIDs {
  144. targetIDs[id] = true
  145. }
  146. // Filter
  147. foundCount := 0
  148. for _, item := range allStates {
  149. if targetIDs[item.EntityID] {
  150. // Basic filtering for invalid states
  151. if item.State != "unknown" && item.State != "unavailable" && item.State != "" {
  152. resultMap[item.EntityID] = item.State
  153. foundCount++
  154. } else {
  155. fmt.Printf("DEBUG: Entity %s found but state is '%s'\n", item.EntityID, item.State)
  156. }
  157. }
  158. }
  159. if foundCount == 0 {
  160. fmt.Printf("DEBUG: Warning - No matching entities found in HA response out of %d requested.\n", len(entityIDs))
  161. // Optional: Print a few available IDs to check for mismatch
  162. if len(allStates) > 0 {
  163. fmt.Printf("DEBUG: Sample available ID: %s\n", allStates[0].EntityID)
  164. }
  165. }
  166. return resultMap, nil
  167. }
  168. // Helpers
  169. func parseHAConfig(config datatypes.JSON) (HAConfig, error) {
  170. var haConfig HAConfig
  171. b, err := config.MarshalJSON()
  172. if err != nil {
  173. return haConfig, fmt.Errorf("config error: %v", err)
  174. }
  175. if err := json.Unmarshal(b, &haConfig); err != nil {
  176. return haConfig, fmt.Errorf("invalid configuration format: %v", err)
  177. }
  178. if haConfig.URL == "" || haConfig.Token == "" {
  179. return haConfig, fmt.Errorf("URL and Token are required")
  180. }
  181. return haConfig, nil
  182. }
  183. func normalizeURL(url string) string {
  184. url = strings.TrimSuffix(url, "/")
  185. url = strings.TrimSuffix(url, "/api")
  186. return url
  187. }
  188. func executeTemplateQuery(client *http.Client, url, token, template string) ([]HAEntity, error) {
  189. reqBody, _ := json.Marshal(HATemplateReq{Template: template})
  190. req, err := http.NewRequest("POST", url+"/api/template", bytes.NewBuffer(reqBody))
  191. if err != nil {
  192. return nil, fmt.Errorf("failed to create request: %v", err)
  193. }
  194. req.Header.Set("Authorization", "Bearer "+token)
  195. req.Header.Set("Content-Type", "application/json")
  196. resp, err := client.Do(req)
  197. if err != nil {
  198. return nil, fmt.Errorf("connection failed: %v", err)
  199. }
  200. defer resp.Body.Close()
  201. if resp.StatusCode != 200 {
  202. return nil, fmt.Errorf("Home Assistant returned status: %s", resp.Status)
  203. }
  204. var tmplResults []HATemplateResult
  205. if err := json.NewDecoder(resp.Body).Decode(&tmplResults); err != nil {
  206. return nil, fmt.Errorf("failed to decode response: %v", err)
  207. }
  208. entities := make([]HAEntity, len(tmplResults))
  209. for i, r := range tmplResults {
  210. entities[i] = HAEntity{
  211. EntityID: r.ID,
  212. State: r.State,
  213. Attributes: map[string]interface{}{"friendly_name": r.Name},
  214. DeviceID: r.DID,
  215. DeviceName: r.DName,
  216. }
  217. }
  218. sort.Slice(entities, func(i, j int) bool {
  219. nameI, _ := entities[i].Attributes["friendly_name"].(string)
  220. nameJ, _ := entities[j].Attributes["friendly_name"].(string)
  221. if nameI == "" { nameI = entities[i].EntityID }
  222. if nameJ == "" { nameJ = entities[j].EntityID }
  223. return nameI < nameJ
  224. })
  225. return entities, nil
  226. }