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.

222 lines
4.6 KiB

2 years ago
  1. package hue
  2. import (
  3. "context"
  4. "errors"
  5. lucifer3 "git.aiterp.net/lucifer3/server"
  6. "git.aiterp.net/lucifer3/server/device"
  7. "git.aiterp.net/lucifer3/server/events"
  8. "git.aiterp.net/lucifer3/server/internal/gentools"
  9. "log"
  10. "strings"
  11. "sync"
  12. "time"
  13. )
  14. func NewBridge(host string, client *Client) *Bridge {
  15. return &Bridge{
  16. client: client,
  17. host: host,
  18. ctx: context.Background(),
  19. cancel: func() {},
  20. resources: map[string]*ResourceData{},
  21. activeStates: map[string]device.State{},
  22. desiredStates: map[string]device.State{},
  23. hasSeen: map[string]bool{},
  24. }
  25. }
  26. type Bridge struct {
  27. mu sync.Mutex
  28. client *Client
  29. host string
  30. ctx context.Context
  31. cancel context.CancelFunc
  32. resources map[string]*ResourceData
  33. activeStates map[string]device.State
  34. desiredStates map[string]device.State
  35. hasSeen map[string]bool
  36. lastDiscoverCancel context.CancelFunc
  37. }
  38. func (b *Bridge) SearchDevices(timeout time.Duration) error {
  39. discoverCtx, cancel := context.WithCancel(b.ctx)
  40. b.mu.Lock()
  41. if b.lastDiscoverCancel != nil {
  42. b.lastDiscoverCancel()
  43. }
  44. b.lastDiscoverCancel = cancel
  45. b.mu.Unlock()
  46. if timeout <= time.Second*10 {
  47. timeout = time.Second * 10
  48. }
  49. // Spend half the time waiting for devices
  50. // TODO: Wait for v2 endpoint
  51. ctx, cancel := context.WithTimeout(discoverCtx, timeout/2)
  52. defer cancel()
  53. err := b.client.LegacyDiscover(ctx, "sensors")
  54. if err != nil {
  55. return err
  56. }
  57. <-ctx.Done()
  58. if discoverCtx.Err() != nil {
  59. return discoverCtx.Err()
  60. }
  61. // Spend half the time waiting for lights
  62. // TODO: Wait for v2 endpoint
  63. ctx, cancel = context.WithTimeout(discoverCtx, timeout/2)
  64. defer cancel()
  65. err = b.client.LegacyDiscover(ctx, "sensors")
  66. if err != nil {
  67. return err
  68. }
  69. <-ctx.Done()
  70. if discoverCtx.Err() != nil {
  71. return discoverCtx.Err()
  72. }
  73. // Let the main loop get the new light.
  74. return nil
  75. }
  76. func (b *Bridge) RefreshAll() ([]lucifer3.Event, error) {
  77. ctx, cancel := context.WithTimeout(b.ctx, time.Second*15)
  78. defer cancel()
  79. allResources, err := b.client.AllResources(ctx)
  80. if err != nil {
  81. return nil, err
  82. }
  83. resources := make(map[string]*ResourceData, len(allResources))
  84. for i := range allResources {
  85. resources[allResources[i].ID] = &allResources[i]
  86. }
  87. newEvents := make([]lucifer3.Event, 0, 0)
  88. b.mu.Lock()
  89. for id, res := range resources {
  90. if res.Type == "device" {
  91. hwState, hwEvent := res.GenerateEvent(b.host, resources)
  92. if !b.hasSeen[id] {
  93. newEvents = append(newEvents, hwState, hwEvent, events.DeviceReady{ID: hwState.ID})
  94. b.hasSeen[id] = true
  95. } else {
  96. newEvents = append(newEvents, hwState)
  97. }
  98. }
  99. }
  100. b.resources = resources
  101. b.mu.Unlock()
  102. return newEvents, nil
  103. }
  104. func (b *Bridge) ApplyPatches(resources []ResourceData) (events []lucifer3.Event, shouldRefresh bool) {
  105. b.mu.Lock()
  106. mapCopy := gentools.CopyMap(b.resources)
  107. b.mu.Unlock()
  108. for _, resource := range resources {
  109. if mapCopy[resource.ID] != nil {
  110. mapCopy[resource.ID] = mapCopy[resource.ID].WithPatch(resource)
  111. } else {
  112. log.Println(resource.ID, resource.Type, "not seen!")
  113. shouldRefresh = true
  114. }
  115. }
  116. for _, resource := range resources {
  117. if resource.Owner != nil && resource.Owner.Kind == "device" {
  118. if parent, ok := mapCopy[resource.Owner.ID]; ok {
  119. hwState, _ := parent.GenerateEvent(b.host, mapCopy)
  120. events = append(events, hwState)
  121. }
  122. }
  123. }
  124. b.mu.Lock()
  125. b.resources = mapCopy
  126. b.mu.Unlock()
  127. return
  128. }
  129. func (b *Bridge) SetStates(patch map[string]device.State) {
  130. b.mu.Lock()
  131. newStates := gentools.CopyMap(b.desiredStates)
  132. resources := b.resources
  133. b.mu.Unlock()
  134. prefix := "hue:" + b.host + ":"
  135. for id, state := range patch {
  136. if !strings.HasPrefix(id, prefix) {
  137. continue
  138. }
  139. id = id[len(prefix):]
  140. resource := resources[id]
  141. if resource == nil {
  142. continue
  143. }
  144. newStates[id] = resource.FixState(state, resources)
  145. }
  146. b.mu.Lock()
  147. b.desiredStates = newStates
  148. b.mu.Unlock()
  149. }
  150. func (b *Bridge) Run(ctx context.Context, bus *lucifer3.EventBus) interface{} {
  151. hwEvents, err := b.RefreshAll()
  152. if err != nil {
  153. return nil
  154. }
  155. bus.RunEvents(hwEvents)
  156. sse := b.client.SSE(ctx)
  157. step := time.NewTicker(time.Second * 30)
  158. defer step.Stop()
  159. for {
  160. select {
  161. case updates, ok := <-sse:
  162. {
  163. if !ok {
  164. return errors.New("SSE lost connection")
  165. }
  166. newEvents, shouldUpdate := b.ApplyPatches(
  167. gentools.Flatten(gentools.Map(updates, func(update SSEUpdate) []ResourceData {
  168. return update.Data
  169. })),
  170. )
  171. bus.RunEvents(newEvents)
  172. if shouldUpdate {
  173. hwEvents, err := b.RefreshAll()
  174. if err != nil {
  175. return nil
  176. }
  177. bus.RunEvents(hwEvents)
  178. }
  179. }
  180. case <-ctx.Done():
  181. {
  182. return nil
  183. }
  184. }
  185. }
  186. }