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.

441 lines
10 KiB

2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
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/color"
  9. "git.aiterp.net/lucifer3/server/internal/gentools"
  10. "golang.org/x/sync/errgroup"
  11. "log"
  12. "math"
  13. "strings"
  14. "sync"
  15. "time"
  16. )
  17. func NewBridge(host string, client *Client) *Bridge {
  18. return &Bridge{
  19. client: client,
  20. host: host,
  21. ctx: context.Background(),
  22. cancel: func() {},
  23. resources: map[string]*ResourceData{},
  24. activeStates: map[string]device.State{},
  25. desiredStates: map[string]device.State{},
  26. colorFlags: map[string]device.ColorFlags{},
  27. hasSeen: map[string]bool{},
  28. triggerCongruenceCheckCh: make(chan struct{}, 2),
  29. }
  30. }
  31. type Bridge struct {
  32. mu sync.Mutex
  33. client *Client
  34. host string
  35. ctx context.Context
  36. cancel context.CancelFunc
  37. resources map[string]*ResourceData
  38. activeStates map[string]device.State
  39. desiredStates map[string]device.State
  40. colorFlags map[string]device.ColorFlags
  41. reachable map[string]bool
  42. hasSeen map[string]bool
  43. triggerCongruenceCheckCh chan struct{}
  44. lastDiscoverCancel context.CancelFunc
  45. }
  46. func (b *Bridge) SearchDevices(timeout time.Duration) error {
  47. discoverCtx, cancel := context.WithCancel(b.ctx)
  48. b.mu.Lock()
  49. if b.lastDiscoverCancel != nil {
  50. b.lastDiscoverCancel()
  51. }
  52. b.lastDiscoverCancel = cancel
  53. b.mu.Unlock()
  54. if timeout <= time.Second*10 {
  55. timeout = time.Second * 10
  56. }
  57. // Spend half the time waiting for devices
  58. // TODO: Wait for v2 endpoint
  59. ctx, cancel := context.WithTimeout(discoverCtx, timeout/2)
  60. defer cancel()
  61. err := b.client.LegacyDiscover(ctx, "sensors")
  62. if err != nil {
  63. return err
  64. }
  65. <-ctx.Done()
  66. if discoverCtx.Err() != nil {
  67. return discoverCtx.Err()
  68. }
  69. // Spend half the time waiting for lights
  70. // TODO: Wait for v2 endpoint
  71. ctx, cancel = context.WithTimeout(discoverCtx, timeout/2)
  72. defer cancel()
  73. err = b.client.LegacyDiscover(ctx, "sensors")
  74. if err != nil {
  75. return err
  76. }
  77. <-ctx.Done()
  78. if discoverCtx.Err() != nil {
  79. return discoverCtx.Err()
  80. }
  81. // Let the main loop get the new light.
  82. return nil
  83. }
  84. func (b *Bridge) RefreshAll() ([]lucifer3.Event, error) {
  85. ctx, cancel := context.WithTimeout(b.ctx, time.Second*15)
  86. defer cancel()
  87. allResources, err := b.client.AllResources(ctx)
  88. if err != nil {
  89. return nil, err
  90. }
  91. resources := make(map[string]*ResourceData, len(allResources))
  92. for i := range allResources {
  93. resources[allResources[i].ID] = &allResources[i]
  94. }
  95. b.mu.Lock()
  96. hasSeen := b.hasSeen
  97. reachable := b.reachable
  98. b.mu.Unlock()
  99. hasSeen = gentools.CopyMap(hasSeen)
  100. reachable = gentools.CopyMap(reachable)
  101. colorFlags := make(map[string]device.ColorFlags)
  102. activeStates := make(map[string]device.State)
  103. newEvents := make([]lucifer3.Event, 0, 0)
  104. for id, res := range resources {
  105. if res.Type == "device" && res.Metadata.Archetype != "bridge_v2" {
  106. hwState, hwEvent := res.GenerateEvent(b.host, resources)
  107. if !hasSeen[id] {
  108. newEvents = append(newEvents, hwState, hwEvent, events.DeviceReady{ID: hwState.ID})
  109. hasSeen[id] = true
  110. } else {
  111. newEvents = append(newEvents, hwState)
  112. }
  113. activeStates[id] = hwState.State
  114. colorFlags[id] = hwState.ColorFlags
  115. reachable[id] = !hwState.Unreachable
  116. }
  117. }
  118. b.mu.Lock()
  119. b.resources = resources
  120. b.hasSeen = hasSeen
  121. b.colorFlags = colorFlags
  122. b.activeStates = activeStates
  123. b.reachable = reachable
  124. b.mu.Unlock()
  125. return newEvents, nil
  126. }
  127. func (b *Bridge) ApplyPatches(resources []ResourceData) (events []lucifer3.Event, shouldRefresh bool) {
  128. b.mu.Lock()
  129. resourceMap := b.resources
  130. activeStates := b.activeStates
  131. reachable := b.reachable
  132. colorFlags := b.colorFlags
  133. b.mu.Unlock()
  134. mapCopy := gentools.CopyMap(resourceMap)
  135. activeStatesCopy := gentools.CopyMap(activeStates)
  136. reachableCopy := gentools.CopyMap(reachable)
  137. colorFlagsCopy := gentools.CopyMap(colorFlags)
  138. for _, resource := range resources {
  139. if mapCopy[resource.ID] != nil {
  140. mapCopy[resource.ID] = mapCopy[resource.ID].WithPatch(resource)
  141. } else {
  142. log.Println(resource.ID, resource.Type, "not seen!")
  143. shouldRefresh = true
  144. }
  145. }
  146. for _, resource := range resources {
  147. if resource.Owner != nil && resource.Owner.Kind == "device" {
  148. if parent, ok := mapCopy[resource.Owner.ID]; ok {
  149. hwState, _ := parent.GenerateEvent(b.host, mapCopy)
  150. events = append(events, hwState)
  151. activeStatesCopy[resource.Owner.ID] = hwState.State
  152. reachableCopy[resource.Owner.ID] = !hwState.Unreachable
  153. if hwState.ColorFlags != 0 {
  154. colorFlagsCopy[resource.Owner.ID] = hwState.ColorFlags
  155. }
  156. }
  157. }
  158. }
  159. b.mu.Lock()
  160. b.resources = mapCopy
  161. b.activeStates = activeStatesCopy
  162. b.reachable = reachableCopy
  163. b.colorFlags = colorFlagsCopy
  164. b.mu.Unlock()
  165. return
  166. }
  167. func (b *Bridge) SetStates(patch map[string]device.State) {
  168. b.mu.Lock()
  169. desiredStates := b.desiredStates
  170. resources := b.resources
  171. b.mu.Unlock()
  172. desiredStates = gentools.CopyMap(desiredStates)
  173. prefix := "hue:" + b.host + ":"
  174. for id, state := range patch {
  175. if !strings.HasPrefix(id, prefix) {
  176. continue
  177. }
  178. id = id[len(prefix):]
  179. resource := resources[id]
  180. if resource == nil {
  181. continue
  182. }
  183. if !state.Empty() {
  184. desiredStates[id] = resource.FixState(state, resources)
  185. } else {
  186. delete(desiredStates, id)
  187. }
  188. }
  189. b.mu.Lock()
  190. b.desiredStates = desiredStates
  191. b.mu.Unlock()
  192. b.triggerCongruenceCheck()
  193. }
  194. func (b *Bridge) Run(ctx context.Context, bus *lucifer3.EventBus) interface{} {
  195. hwEvents, err := b.RefreshAll()
  196. if err != nil {
  197. return errors.New("failed to connect to bridge")
  198. }
  199. go b.makeCongruentLoop(ctx)
  200. bus.RunEvents(hwEvents)
  201. sse := b.client.SSE(ctx)
  202. step := time.NewTicker(time.Second * 30)
  203. defer step.Stop()
  204. for {
  205. select {
  206. case updates, ok := <-sse:
  207. {
  208. if !ok {
  209. return errors.New("SSE lost connection")
  210. }
  211. newEvents, shouldUpdate := b.ApplyPatches(
  212. gentools.Flatten(gentools.Map(updates, func(update SSEUpdate) []ResourceData {
  213. return update.Data
  214. })),
  215. )
  216. bus.RunEvents(newEvents)
  217. if shouldUpdate {
  218. hwEvents, err := b.RefreshAll()
  219. if err != nil {
  220. return errors.New("failed to refresh states")
  221. }
  222. bus.RunEvents(hwEvents)
  223. }
  224. b.triggerCongruenceCheck()
  225. }
  226. case <-step.C:
  227. hwEvents, err := b.RefreshAll()
  228. if err != nil {
  229. return nil
  230. }
  231. bus.RunEvents(hwEvents)
  232. b.triggerCongruenceCheck()
  233. case <-ctx.Done():
  234. {
  235. return nil
  236. }
  237. }
  238. }
  239. }
  240. func (b *Bridge) makeCongruentLoop(ctx context.Context) {
  241. for range b.triggerCongruenceCheckCh {
  242. if ctx.Err() != nil {
  243. break
  244. }
  245. // Make sure this loop doesn't spam too hard
  246. rateLimit := time.After(time.Second / 15)
  247. // Take states
  248. b.mu.Lock()
  249. resources := b.resources
  250. desiredStates := b.desiredStates
  251. activeStates := b.activeStates
  252. reachable := b.reachable
  253. colorFlags := b.colorFlags
  254. b.mu.Unlock()
  255. newActiveStates := make(map[string]device.State, 0)
  256. updates := make(map[string]ResourceUpdate)
  257. for id, desired := range desiredStates {
  258. active, activeOK := activeStates[id]
  259. lightID := resources[id].ServiceID("light")
  260. if !reachable[id] || !activeOK || lightID == nil {
  261. log.Println("No light", !reachable[id], !activeOK, lightID == nil)
  262. continue
  263. }
  264. light := resources[*lightID]
  265. if light == nil {
  266. log.Println("No light", *lightID)
  267. continue
  268. }
  269. // Handle power first
  270. if desired.Power != nil && active.Power != nil && *desired.Power != *active.Power {
  271. updates["light/"+*lightID] = ResourceUpdate{Power: gentools.Ptr(*desired.Power)}
  272. newActiveState := activeStates[id]
  273. newActiveState.Power = gentools.Ptr(*desired.Power)
  274. newActiveStates[id] = newActiveState
  275. continue
  276. }
  277. if active.Power != nil && !*active.Power {
  278. // Don't do more with shut-off-light.
  279. continue
  280. }
  281. updated := false
  282. update := ResourceUpdate{}
  283. newActiveState := activeStates[id]
  284. if active.Color != nil && desired.Color != nil {
  285. ac := *active.Color
  286. dc := *desired.Color
  287. if !dc.IsKelvin() || !colorFlags[id].IsWarmWhite() {
  288. dc, _ = dc.ToXY()
  289. dc.XY = gentools.Ptr(light.Color.Gamut.Conform(*dc.XY))
  290. }
  291. if dc.XY != nil {
  292. if ac.K != nil {
  293. ac.K = gentools.Ptr(1000000 / (1000000 / *ac.K))
  294. }
  295. acXY, _ := ac.ToXY()
  296. dist := dc.XY.DistanceTo(*acXY.XY)
  297. if dist > 0.0002 {
  298. update.ColorXY = gentools.Ptr(*dc.XY)
  299. updated = true
  300. }
  301. } else {
  302. dcMirek := 1000000 / *dc.K
  303. if dcMirek < light.ColorTemperature.MirekSchema.MirekMinimum {
  304. dcMirek = light.ColorTemperature.MirekSchema.MirekMinimum
  305. } else if dcMirek > light.ColorTemperature.MirekSchema.MirekMaximum {
  306. dcMirek = light.ColorTemperature.MirekSchema.MirekMaximum
  307. }
  308. acMirek := 0
  309. if ac.K != nil {
  310. acMirek = 1000000 / *ac.K
  311. }
  312. if acMirek != dcMirek {
  313. newActiveState.Color = &color.Color{K: gentools.Ptr(*dc.K)}
  314. update.Mirek = &dcMirek
  315. updated = true
  316. }
  317. }
  318. }
  319. if active.Intensity != nil && desired.Intensity != nil {
  320. if math.Abs(*active.Intensity-*desired.Intensity) >= 0.01 {
  321. update.Brightness = gentools.Ptr(*desired.Intensity * 100)
  322. newActiveState.Intensity = gentools.Ptr(*desired.Intensity)
  323. updated = true
  324. }
  325. }
  326. if updated {
  327. update.TransitionDuration = gentools.Ptr(time.Millisecond * 101)
  328. updates["light/"+*lightID] = update
  329. newActiveStates[id] = newActiveState
  330. }
  331. }
  332. if len(updates) > 0 {
  333. timeout, cancel := context.WithTimeout(ctx, time.Second)
  334. eg, ctx := errgroup.WithContext(timeout)
  335. for key := range updates {
  336. update := updates[key]
  337. split := strings.SplitN(key, "/", 2)
  338. link := ResourceLink{Kind: split[0], ID: split[1]}
  339. eg.Go(func() error {
  340. return b.client.UpdateResource(ctx, link, update)
  341. })
  342. }
  343. err := eg.Wait()
  344. if err != nil {
  345. log.Println("Failed to run update", err)
  346. }
  347. b.mu.Lock()
  348. activeStates = b.activeStates
  349. b.mu.Unlock()
  350. activeStates = gentools.CopyMap(activeStates)
  351. for id, state := range newActiveStates {
  352. activeStates[id] = state
  353. }
  354. b.mu.Lock()
  355. b.activeStates = activeStates
  356. b.mu.Unlock()
  357. cancel()
  358. // Wait the remaining time for the rate limit
  359. <-rateLimit
  360. }
  361. }
  362. }
  363. func (b *Bridge) triggerCongruenceCheck() {
  364. select {
  365. case b.triggerCongruenceCheckCh <- struct{}{}:
  366. default:
  367. }
  368. }