ha_client.go 6.6 KB

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