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.

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