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.

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