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.

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