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.

425 lines
8.6 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. "strconv"
  13. "strings"
  14. "sync"
  15. "time"
  16. )
  17. type Bridge struct {
  18. mu sync.Mutex
  19. host string
  20. token string
  21. externalID int
  22. lightStates []*hueLightState
  23. sensorStates []*hueSensorState
  24. quarantine map[string]time.Time
  25. syncingPublish uint32
  26. }
  27. func (b *Bridge) Refresh(ctx context.Context) error {
  28. lightMap, err := b.getLights(ctx)
  29. if err != nil {
  30. return err
  31. }
  32. b.mu.Lock()
  33. for index, light := range lightMap {
  34. if time.Now().Before(b.quarantine[light.Uniqueid]) {
  35. continue
  36. }
  37. var state *hueLightState
  38. for _, existingState := range b.lightStates {
  39. if existingState.index == index {
  40. state = existingState
  41. }
  42. }
  43. if state == nil {
  44. state = &hueLightState{
  45. index: index,
  46. uniqueID: light.Uniqueid,
  47. externalID: -1,
  48. info: light,
  49. }
  50. b.lightStates = append(b.lightStates, state)
  51. } else {
  52. if light.Uniqueid != state.uniqueID {
  53. state.uniqueID = light.Uniqueid
  54. state.externalID = -1
  55. }
  56. }
  57. state.CheckStaleness(light.State)
  58. }
  59. b.mu.Unlock()
  60. sensorMap, err := b.getSensors(ctx)
  61. if err != nil {
  62. return err
  63. }
  64. b.mu.Lock()
  65. for index, sensor := range sensorMap {
  66. if time.Now().Before(b.quarantine[sensor.UniqueID]) {
  67. continue
  68. }
  69. var state *hueSensorState
  70. for _, existingState := range b.sensorStates {
  71. if existingState.index == index {
  72. state = existingState
  73. }
  74. }
  75. if state == nil {
  76. state = &hueSensorState{
  77. index: index,
  78. uniqueID: sensor.UniqueID,
  79. externalID: -1,
  80. presenceCooldown: -2,
  81. }
  82. b.sensorStates = append(b.sensorStates, state)
  83. } else {
  84. if sensor.UniqueID != state.uniqueID {
  85. state.uniqueID = sensor.UniqueID
  86. state.externalID = -1
  87. }
  88. }
  89. }
  90. b.mu.Unlock()
  91. return nil
  92. }
  93. func (b *Bridge) SyncStale(ctx context.Context) error {
  94. indices := make([]int, 0, 4)
  95. inputs := make([]LightStateInput, 0, 4)
  96. eg, ctx := errgroup.WithContext(ctx)
  97. b.mu.Lock()
  98. for _, state := range b.lightStates {
  99. if !state.stale {
  100. continue
  101. }
  102. indices = append(indices, state.index)
  103. inputs = append(inputs, state.input)
  104. }
  105. b.mu.Unlock()
  106. if len(inputs) == 0 {
  107. return nil
  108. }
  109. for i, input := range inputs {
  110. iCopy := i
  111. index := indices[i]
  112. inputCopy := input
  113. eg.Go(func() error {
  114. err := b.putLightState(ctx, index, inputCopy)
  115. if err != nil {
  116. return err
  117. }
  118. b.lightStates[iCopy].stale = false
  119. return nil
  120. })
  121. }
  122. return eg.Wait()
  123. }
  124. func (b *Bridge) ForgetDevice(ctx context.Context, device models.Device) error {
  125. // Find index
  126. b.mu.Lock()
  127. found := false
  128. index := -1
  129. for i, ls := range b.lightStates {
  130. if ls.uniqueID == device.InternalID {
  131. found = true
  132. index = i
  133. }
  134. }
  135. b.mu.Unlock()
  136. if !found {
  137. return models.ErrNotFound
  138. }
  139. // Delete light from bridge
  140. err := b.deleteLight(ctx, index)
  141. if err != nil {
  142. return err
  143. }
  144. // Remove light state from local list. I don't know if the quarantine is necessary, but let's have it anyway.
  145. b.mu.Lock()
  146. for i, ls := range b.lightStates {
  147. if ls.uniqueID == device.InternalID {
  148. b.lightStates = append(b.lightStates[:i], b.lightStates[i+1:]...)
  149. }
  150. }
  151. if b.quarantine == nil {
  152. b.quarantine = make(map[string]time.Time, 1)
  153. }
  154. b.quarantine[device.InternalID] = time.Now().Add(time.Second * 30)
  155. b.mu.Unlock()
  156. return nil
  157. }
  158. func (b *Bridge) SyncSensors(ctx context.Context) ([]models.Event, error) {
  159. sensorMap, err := b.getSensors(ctx)
  160. if err != nil {
  161. return nil, err
  162. }
  163. var events []models.Event
  164. b.mu.Lock()
  165. for idx, sensorData := range sensorMap {
  166. for _, state := range b.sensorStates {
  167. if idx == state.index {
  168. event := state.Update(sensorData)
  169. if event != nil {
  170. events = append(events, *event)
  171. }
  172. break
  173. }
  174. }
  175. }
  176. b.mu.Unlock()
  177. return events, nil
  178. }
  179. func (b *Bridge) StartDiscovery(ctx context.Context, model string) error {
  180. return b.post(ctx, model, nil, nil)
  181. }
  182. func (b *Bridge) putLightState(ctx context.Context, index int, input LightStateInput) error {
  183. return b.put(ctx, fmt.Sprintf("lights/%d/state", index), input, nil)
  184. }
  185. func (b *Bridge) putGroupLightState(ctx context.Context, index int, input LightStateInput) error {
  186. return b.put(ctx, fmt.Sprintf("groups/%d/action", index), input, nil)
  187. }
  188. func (b *Bridge) deleteLight(ctx context.Context, index int) error {
  189. return b.delete(ctx, fmt.Sprintf("lights/%d", index), nil)
  190. }
  191. func (b *Bridge) getToken(ctx context.Context) (string, error) {
  192. result := make([]CreateUserResponse, 0, 1)
  193. err := b.post(ctx, "", CreateUserInput{DeviceType: "git.aiterp.net/lucifer"}, &result)
  194. if err != nil {
  195. return "", err
  196. }
  197. if len(result) == 0 || result[0].Error != nil {
  198. return "", errLinkButtonNotPressed
  199. }
  200. if result[0].Success == nil {
  201. return "", models.ErrUnexpectedResponse
  202. }
  203. return result[0].Success.Username, nil
  204. }
  205. func (b *Bridge) getLights(ctx context.Context) (map[int]LightData, error) {
  206. result := make(map[int]LightData, 16)
  207. err := b.get(ctx, "lights", &result)
  208. if err != nil {
  209. return nil, err
  210. }
  211. return result, nil
  212. }
  213. func (b *Bridge) getSensors(ctx context.Context) (map[int]SensorData, error) {
  214. result := make(map[int]SensorData, 16)
  215. err := b.get(ctx, "sensors", &result)
  216. if err != nil {
  217. return nil, err
  218. }
  219. return result, nil
  220. }
  221. func (b *Bridge) getGroups(ctx context.Context) (map[int]GroupData, error) {
  222. result := make(map[int]GroupData, 16)
  223. err := b.get(ctx, "groups", &result)
  224. if err != nil {
  225. return nil, err
  226. }
  227. return result, nil
  228. }
  229. func (b *Bridge) postGroup(ctx context.Context, input GroupData) (int, error) {
  230. var res []struct {
  231. Success struct {
  232. ID string `json:"id"`
  233. } `json:"success"`
  234. }
  235. err := b.post(ctx, "groups", input, &res)
  236. id, _ := strconv.Atoi(res[0].Success.ID)
  237. return id, err
  238. }
  239. func (b *Bridge) deleteGroup(ctx context.Context, index int) error {
  240. return b.delete(ctx, "groups/"+strconv.Itoa(index), nil)
  241. }
  242. func (b *Bridge) get(ctx context.Context, resource string, target interface{}) error {
  243. if b.token != "" {
  244. resource = b.token + "/" + resource
  245. }
  246. req, err := http.NewRequest("GET", fmt.Sprintf("http://%s/api/%s", b.host, resource), nil)
  247. if err != nil {
  248. return err
  249. }
  250. res, err := httpClient.Do(req.WithContext(ctx))
  251. if err != nil {
  252. return err
  253. }
  254. defer res.Body.Close()
  255. return json.NewDecoder(res.Body).Decode(target)
  256. }
  257. func (b *Bridge) delete(ctx context.Context, resource string, target interface{}) error {
  258. if b.token != "" {
  259. resource = b.token + "/" + resource
  260. }
  261. req, err := http.NewRequest("DELETE", fmt.Sprintf("http://%s/api/%s", b.host, resource), nil)
  262. if err != nil {
  263. return err
  264. }
  265. res, err := httpClient.Do(req.WithContext(ctx))
  266. if err != nil {
  267. return err
  268. }
  269. defer res.Body.Close()
  270. if target == nil {
  271. return nil
  272. }
  273. return json.NewDecoder(res.Body).Decode(target)
  274. }
  275. func (b *Bridge) post(ctx context.Context, resource string, body interface{}, target interface{}) error {
  276. rb, err := reqBody(body)
  277. if err != nil {
  278. return err
  279. }
  280. if b.token != "" {
  281. resource = b.token + "/" + resource
  282. }
  283. req, err := http.NewRequest("POST", fmt.Sprintf("http://%s/api/%s", b.host, resource), rb)
  284. if err != nil {
  285. return err
  286. }
  287. res, err := httpClient.Do(req.WithContext(ctx))
  288. if err != nil {
  289. return err
  290. }
  291. defer res.Body.Close()
  292. if target == nil {
  293. return nil
  294. }
  295. return json.NewDecoder(res.Body).Decode(target)
  296. }
  297. func (b *Bridge) put(ctx context.Context, resource string, body interface{}, target interface{}) error {
  298. rb, err := reqBody(body)
  299. if err != nil {
  300. return err
  301. }
  302. if b.token != "" {
  303. resource = b.token + "/" + resource
  304. }
  305. req, err := http.NewRequest("PUT", fmt.Sprintf("http://%s/api/%s", b.host, resource), rb)
  306. if err != nil {
  307. return err
  308. }
  309. res, err := httpClient.Do(req.WithContext(ctx))
  310. if err != nil {
  311. return err
  312. }
  313. defer res.Body.Close()
  314. if target == nil {
  315. return nil
  316. }
  317. return json.NewDecoder(res.Body).Decode(target)
  318. }
  319. func reqBody(body interface{}) (io.Reader, error) {
  320. if body == nil {
  321. return nil, nil
  322. }
  323. switch v := body.(type) {
  324. case []byte:
  325. return bytes.NewReader(v), nil
  326. case string:
  327. return strings.NewReader(v), nil
  328. case io.Reader:
  329. return v, nil
  330. default:
  331. jsonData, err := json.Marshal(v)
  332. if err != nil {
  333. return nil, err
  334. }
  335. return bytes.NewReader(jsonData), nil
  336. }
  337. }
  338. var httpClient = &http.Client{
  339. Transport: &http.Transport{
  340. Proxy: http.ProxyFromEnvironment,
  341. DialContext: (&net.Dialer{
  342. Timeout: 30 * time.Second,
  343. KeepAlive: 30 * time.Second,
  344. }).DialContext,
  345. MaxIdleConns: 256,
  346. MaxIdleConnsPerHost: 16,
  347. IdleConnTimeout: 10 * time.Minute,
  348. },
  349. Timeout: time.Minute,
  350. }