You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

291 lines
5.7 KiB

  1. package hue
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "fmt"
  7. "git.aiterp.net/lucifer/new-server/models"
  8. "golang.org/x/sync/errgroup"
  9. "io"
  10. "net/http"
  11. "strings"
  12. "sync"
  13. )
  14. type Bridge struct {
  15. mu sync.Mutex
  16. host string
  17. token string
  18. externalID int
  19. lightStates []*hueLightState
  20. sensorStates []*hueSensorState
  21. }
  22. func (b *Bridge) Refresh(ctx context.Context) error {
  23. lightMap, err := b.getLights(ctx)
  24. if err != nil {
  25. return err
  26. }
  27. b.mu.Lock()
  28. for index, light := range lightMap {
  29. var state *hueLightState
  30. for _, existingState := range b.lightStates {
  31. if existingState.index == index {
  32. state = existingState
  33. }
  34. }
  35. if state == nil {
  36. state = &hueLightState{
  37. index: index,
  38. uniqueID: light.Uniqueid,
  39. externalID: -1,
  40. info: light,
  41. }
  42. b.lightStates = append(b.lightStates, state)
  43. } else {
  44. if light.Uniqueid != state.uniqueID {
  45. state.uniqueID = light.Uniqueid
  46. state.externalID = -1
  47. }
  48. }
  49. state.CheckStaleness(light.State)
  50. }
  51. b.mu.Unlock()
  52. sensorMap, err := b.getSensors(ctx)
  53. if err != nil {
  54. return err
  55. }
  56. b.mu.Lock()
  57. for index, sensor := range sensorMap {
  58. var state *hueSensorState
  59. for _, existingState := range b.sensorStates {
  60. if existingState.index == index {
  61. state = existingState
  62. }
  63. }
  64. if state == nil {
  65. state = &hueSensorState{
  66. index: index,
  67. uniqueID: sensor.UniqueID,
  68. externalID: -1,
  69. }
  70. b.sensorStates = append(b.sensorStates, state)
  71. } else {
  72. if sensor.UniqueID != state.uniqueID {
  73. state.uniqueID = sensor.UniqueID
  74. state.externalID = -1
  75. }
  76. }
  77. }
  78. b.mu.Unlock()
  79. return nil
  80. }
  81. func (b *Bridge) SyncStale(ctx context.Context) error {
  82. indices := make([]int, 0, 4)
  83. inputs := make([]LightStateInput, 0, 4)
  84. eg, ctx := errgroup.WithContext(ctx)
  85. b.mu.Lock()
  86. for _, state := range b.lightStates {
  87. if !state.stale {
  88. continue
  89. }
  90. indices = append(indices, state.index)
  91. inputs = append(inputs, state.input)
  92. }
  93. b.mu.Unlock()
  94. if len(inputs) == 0 {
  95. return nil
  96. }
  97. for i, input := range inputs {
  98. index := indices[i]
  99. inputCopy := input
  100. eg.Go(func() error { return b.putLightState(ctx, index, inputCopy) })
  101. }
  102. return eg.Wait()
  103. }
  104. func (b *Bridge) SyncSensors(ctx context.Context) ([]models.Event, error) {
  105. sensorMap, err := b.getSensors(ctx)
  106. if err != nil {
  107. return nil, err
  108. }
  109. var events []models.Event
  110. b.mu.Lock()
  111. for idx, sensorData := range sensorMap {
  112. for _, state := range b.sensorStates {
  113. if idx == state.index {
  114. event := state.Update(sensorData)
  115. if event != nil {
  116. events = append(events, *event)
  117. }
  118. break
  119. }
  120. }
  121. }
  122. b.mu.Unlock()
  123. return events, nil
  124. }
  125. func (b *Bridge) StartDiscovery(ctx context.Context, model string) error {
  126. return b.post(ctx, model, nil, nil)
  127. }
  128. func (b *Bridge) putLightState(ctx context.Context, index int, input LightStateInput) error {
  129. return b.put(ctx, fmt.Sprintf("lights/%d/state", index), input, nil)
  130. }
  131. func (b *Bridge) getToken(ctx context.Context) (string, error) {
  132. result := make([]CreateUserResponse, 0, 1)
  133. err := b.post(ctx, "", CreateUserInput{DeviceType: "git.aiterp.net/lucifer"}, &result)
  134. if err != nil {
  135. return "", err
  136. }
  137. if len(result) == 0 || result[0].Error != nil {
  138. return "", errLinkButtonNotPressed
  139. }
  140. if result[0].Success == nil {
  141. return "", models.ErrUnexpectedResponse
  142. }
  143. return result[0].Success.Username, nil
  144. }
  145. func (b *Bridge) getLights(ctx context.Context) (map[int]LightData, error) {
  146. result := make(map[int]LightData, 16)
  147. err := b.get(ctx, "lights", &result)
  148. if err != nil {
  149. return nil, err
  150. }
  151. return result, nil
  152. }
  153. func (b *Bridge) getSensors(ctx context.Context) (map[int]SensorData, error) {
  154. result := make(map[int]SensorData, 16)
  155. err := b.get(ctx, "sensors", &result)
  156. if err != nil {
  157. return nil, err
  158. }
  159. return result, nil
  160. }
  161. func (b *Bridge) get(ctx context.Context, resource string, target interface{}) error {
  162. if b.token != "" {
  163. resource = b.token + "/" + resource
  164. }
  165. req, err := http.NewRequest("GET", fmt.Sprintf("http://%s/api/%s", b.host, resource), nil)
  166. if err != nil {
  167. return err
  168. }
  169. res, err := http.DefaultClient.Do(req.WithContext(ctx))
  170. if err != nil {
  171. return err
  172. }
  173. defer res.Body.Close()
  174. return json.NewDecoder(res.Body).Decode(target)
  175. }
  176. func (b *Bridge) post(ctx context.Context, resource string, body interface{}, target interface{}) error {
  177. rb, err := reqBody(body)
  178. if err != nil {
  179. return err
  180. }
  181. if b.token != "" {
  182. resource = b.token + "/" + resource
  183. }
  184. req, err := http.NewRequest("POST", fmt.Sprintf("http://%s/api/%s", b.host, resource), rb)
  185. if err != nil {
  186. return err
  187. }
  188. res, err := http.DefaultClient.Do(req.WithContext(ctx))
  189. if err != nil {
  190. return err
  191. }
  192. defer res.Body.Close()
  193. if target == nil {
  194. return nil
  195. }
  196. return json.NewDecoder(res.Body).Decode(target)
  197. }
  198. func (b *Bridge) put(ctx context.Context, resource string, body interface{}, target interface{}) error {
  199. rb, err := reqBody(body)
  200. if err != nil {
  201. return err
  202. }
  203. if b.token != "" {
  204. resource = b.token + "/" + resource
  205. }
  206. req, err := http.NewRequest("PUT", fmt.Sprintf("http://%s/api/%s", b.host, resource), rb)
  207. if err != nil {
  208. return err
  209. }
  210. res, err := http.DefaultClient.Do(req.WithContext(ctx))
  211. if err != nil {
  212. return err
  213. }
  214. defer res.Body.Close()
  215. if target == nil {
  216. return nil
  217. }
  218. return json.NewDecoder(res.Body).Decode(target)
  219. }
  220. func reqBody(body interface{}) (io.Reader, error) {
  221. if body == nil {
  222. return nil, nil
  223. }
  224. switch v := body.(type) {
  225. case []byte:
  226. return bytes.NewReader(v), nil
  227. case string:
  228. return strings.NewReader(v), nil
  229. case io.Reader:
  230. return v, nil
  231. default:
  232. jsonData, err := json.Marshal(v)
  233. if err != nil {
  234. return nil, err
  235. }
  236. return bytes.NewReader(jsonData), nil
  237. }
  238. }