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.

593 lines
14 KiB

2 years ago
2 years ago
2 years ago
2 years ago
1 year ago
1 year ago
2 years ago
2 years ago
1 year ago
2 years ago
2 years ago
1 year ago
1 year ago
2 years ago
1 year ago
1 year ago
1 year ago
2 years ago
2 years ago
2 years ago
2 years ago
1 year ago
2 years ago
2 years ago
2 years ago
1 year ago
2 years ago
1 year ago
2 years ago
2 years ago
1 year ago
2 years ago
2 years ago
1 year ago
1 year ago
1 year ago
1 year ago
2 years ago
1 year 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 year ago
1 year 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. "fmt"
  6. lucifer3 "git.aiterp.net/lucifer3/server"
  7. "git.aiterp.net/lucifer3/server/device"
  8. "git.aiterp.net/lucifer3/server/events"
  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. mu: sync.Mutex{},
  20. client: client,
  21. host: host,
  22. ctx: context.Background(),
  23. cancel: func() {},
  24. resources: map[string]*ResourceData{},
  25. activeStates: map[string]device.State{},
  26. desiredStates: map[string]device.State{},
  27. colorFlags: map[string]device.ColorFlags{},
  28. reachable: nil,
  29. hasSeen: map[string]bool{},
  30. triggerCongruenceCheckCh: make(chan struct{}, 2),
  31. }
  32. }
  33. type Bridge struct {
  34. mu sync.Mutex
  35. client *Client
  36. host string
  37. ctx context.Context
  38. cancel context.CancelFunc
  39. resources map[string]*ResourceData
  40. activeStates map[string]device.State
  41. desiredStates map[string]device.State
  42. colorFlags map[string]device.ColorFlags
  43. reachable map[string]bool
  44. hasSeen map[string]bool
  45. lastMotion map[string]time.Time
  46. lastPresence map[string]bool
  47. lastButton map[string]time.Time
  48. triggerCongruenceCheckCh chan struct{}
  49. lastDiscoverCancel context.CancelFunc
  50. }
  51. func (b *Bridge) SearchDevices(timeout time.Duration) error {
  52. discoverCtx, cancel := context.WithCancel(b.ctx)
  53. b.mu.Lock()
  54. if b.lastDiscoverCancel != nil {
  55. b.lastDiscoverCancel()
  56. }
  57. b.lastDiscoverCancel = cancel
  58. b.mu.Unlock()
  59. if timeout <= time.Second*10 {
  60. timeout = time.Second * 10
  61. }
  62. // Spend half the time waiting for devices
  63. // TODO: Wait for v2 endpoint
  64. ctx, cancel := context.WithTimeout(discoverCtx, timeout/2)
  65. defer cancel()
  66. err := b.client.LegacyDiscover(ctx, "sensors")
  67. if err != nil {
  68. return err
  69. }
  70. <-ctx.Done()
  71. if discoverCtx.Err() != nil {
  72. return discoverCtx.Err()
  73. }
  74. // Spend half the time waiting for lights
  75. // TODO: Wait for v2 endpoint
  76. ctx, cancel = context.WithTimeout(discoverCtx, timeout/2)
  77. defer cancel()
  78. err = b.client.LegacyDiscover(ctx, "sensors")
  79. if err != nil {
  80. return err
  81. }
  82. <-ctx.Done()
  83. if discoverCtx.Err() != nil {
  84. return discoverCtx.Err()
  85. }
  86. // Let the main loop get the new light.
  87. return nil
  88. }
  89. func (b *Bridge) RefreshAll() ([]lucifer3.Event, error) {
  90. ctx, cancel := context.WithTimeout(b.ctx, time.Second*15)
  91. defer cancel()
  92. allResources, err := b.client.AllResources(ctx)
  93. if err != nil {
  94. return nil, err
  95. }
  96. resources := make(map[string]*ResourceData, len(allResources))
  97. for i := range allResources {
  98. resources[allResources[i].ID] = &allResources[i]
  99. }
  100. b.mu.Lock()
  101. hasSeen := b.hasSeen
  102. reachable := b.reachable
  103. lastMotion := b.lastMotion
  104. lastPresence := b.lastPresence
  105. b.mu.Unlock()
  106. oldHasSeen := hasSeen
  107. hasSeen = gentools.CopyMap(hasSeen)
  108. reachable = gentools.CopyMap(reachable)
  109. lastMotion = gentools.CopyMap(lastMotion)
  110. lastPresence = gentools.CopyMap(lastPresence)
  111. colorFlags := make(map[string]device.ColorFlags)
  112. activeStates := make(map[string]device.State)
  113. newEvents := make([]lucifer3.Event, 0, 0)
  114. extraEvents := make([]lucifer3.Event, 0, 0)
  115. for id, res := range resources {
  116. if res.Owner != nil && !oldHasSeen[res.Owner.ID] {
  117. if res.Temperature != nil {
  118. extraEvents = append(extraEvents, events.TemperatureChanged{
  119. ID: b.fullId(*res),
  120. Temperature: res.Temperature.Temperature,
  121. })
  122. }
  123. if res.Motion != nil {
  124. if res.Motion.Motion {
  125. extraEvents = append(extraEvents, events.MotionSensed{
  126. ID: b.fullId(*res),
  127. SecondsSince: 0,
  128. })
  129. lastMotion[b.fullId(*res)] = time.Now()
  130. lastPresence[b.fullId(*res)] = true
  131. } else {
  132. if lastMotion[b.fullId(*res)].IsZero() {
  133. extraEvents = append(extraEvents, events.MotionSensed{
  134. ID: b.fullId(*res),
  135. SecondsSince: 301,
  136. })
  137. lastMotion[b.fullId(*res)] = time.Now().Add(-time.Millisecond * 301)
  138. }
  139. lastPresence[b.fullId(*res)] = false
  140. }
  141. }
  142. }
  143. if res.Type == "device" && !strings.HasPrefix(res.Metadata.Archetype, "bridge") {
  144. hwState, hwEvent := res.GenerateEvent(b.host, resources)
  145. if hwState.SupportFlags == 0 {
  146. continue
  147. }
  148. newEvents = append(newEvents, hwState)
  149. if !hasSeen[id] {
  150. newEvents = append(newEvents, hwEvent, events.DeviceReady{ID: hwState.ID})
  151. hasSeen[id] = true
  152. }
  153. activeStates[id] = hwState.State
  154. colorFlags[id] = hwState.ColorFlags
  155. reachable[id] = !hwState.Unreachable
  156. }
  157. }
  158. b.mu.Lock()
  159. b.resources = resources
  160. b.hasSeen = hasSeen
  161. b.colorFlags = colorFlags
  162. b.activeStates = activeStates
  163. b.reachable = reachable
  164. b.lastMotion = lastMotion
  165. b.lastPresence = lastPresence
  166. b.mu.Unlock()
  167. return append(newEvents, extraEvents...), nil
  168. }
  169. func (b *Bridge) ApplyPatches(date time.Time, resources []ResourceData) (eventList []lucifer3.Event, shouldRefresh bool) {
  170. b.mu.Lock()
  171. resourceMap := b.resources
  172. activeStates := b.activeStates
  173. reachable := b.reachable
  174. colorFlags := b.colorFlags
  175. lastMotion := b.lastMotion
  176. lastButton := b.lastButton
  177. lastPresence := b.lastPresence
  178. b.mu.Unlock()
  179. mapCopy := gentools.CopyMap(resourceMap)
  180. activeStatesCopy := gentools.CopyMap(activeStates)
  181. reachableCopy := gentools.CopyMap(reachable)
  182. colorFlagsCopy := gentools.CopyMap(colorFlags)
  183. lastMotionCopy := gentools.CopyMap(lastMotion)
  184. lastButtonCopy := gentools.CopyMap(lastButton)
  185. lastPresenceCopy := gentools.CopyMap(lastPresence)
  186. for _, resource := range resources {
  187. if mapCopy[resource.ID] != nil {
  188. mapCopy[resource.ID] = mapCopy[resource.ID].WithPatch(resource)
  189. } else {
  190. eventList = append(eventList, events.Log{
  191. ID: b.fullId(resource),
  192. Level: "info",
  193. Code: "hue_patch_found_unknown_device",
  194. Message: "Refresh triggered, because of unknown device",
  195. })
  196. shouldRefresh = true
  197. }
  198. }
  199. for _, resource := range resources {
  200. fullID := b.fullId(resource)
  201. if resource.Owner != nil && resource.Owner.Kind == "device" {
  202. if parent, ok := mapCopy[resource.Owner.ID]; ok && !strings.HasPrefix(parent.Metadata.Archetype, "bridge") {
  203. hwState, _ := parent.GenerateEvent(b.host, mapCopy)
  204. eventList = append(eventList, hwState)
  205. activeStatesCopy[resource.Owner.ID] = hwState.State
  206. reachableCopy[resource.Owner.ID] = !hwState.Unreachable
  207. if hwState.ColorFlags != 0 {
  208. colorFlagsCopy[resource.Owner.ID] = hwState.ColorFlags
  209. }
  210. if resource.Temperature != nil {
  211. eventList = append(eventList, events.TemperatureChanged{
  212. ID: fullID,
  213. Temperature: resource.Temperature.Temperature,
  214. })
  215. }
  216. if resource.Motion != nil {
  217. lastMotionCopy[fullID] = time.Now()
  218. if resource.Motion.Motion {
  219. eventList = append(eventList, events.MotionSensed{
  220. ID: fullID,
  221. SecondsSince: 0,
  222. })
  223. lastMotion[fullID] = time.Now()
  224. lastPresenceCopy[fullID] = true
  225. } else if lastPresenceCopy[fullID] {
  226. lastMotion[fullID] = time.Now()
  227. lastPresenceCopy[fullID] = false
  228. }
  229. }
  230. if resource.Button != nil {
  231. valid := false
  232. if resource.Button.LastEvent == "initial_press" {
  233. valid = true
  234. } else if resource.Button.LastEvent == "long_release" {
  235. valid = false
  236. } else if resource.Button.LastEvent == "repeat" {
  237. valid = date.Sub(lastButtonCopy[resource.ID]) >= time.Millisecond*500
  238. } else {
  239. valid = date.Sub(lastButtonCopy[resource.ID]) >= time.Millisecond*990
  240. }
  241. if valid {
  242. lastButtonCopy[resource.ID] = date
  243. owner := resourceMap[resource.Owner.ID]
  244. if owner != nil {
  245. index := owner.ServiceIndex("button", resource.ID)
  246. if index != -1 {
  247. eventList = append(eventList, events.ButtonPressed{
  248. ID: b.fullId(*owner),
  249. Name: []string{"On", "DimUp", "DimDown", "Off"}[index],
  250. })
  251. }
  252. }
  253. }
  254. }
  255. } else {
  256. shouldRefresh = true
  257. }
  258. }
  259. }
  260. b.mu.Lock()
  261. b.resources = mapCopy
  262. b.activeStates = activeStatesCopy
  263. b.reachable = reachableCopy
  264. b.colorFlags = colorFlagsCopy
  265. b.lastMotion = lastMotionCopy
  266. b.lastButton = lastButtonCopy
  267. b.lastPresence = lastPresenceCopy
  268. b.mu.Unlock()
  269. return
  270. }
  271. func (b *Bridge) SetStates(patch map[string]device.State) {
  272. b.mu.Lock()
  273. desiredStates := b.desiredStates
  274. resources := b.resources
  275. b.mu.Unlock()
  276. desiredStates = gentools.CopyMap(desiredStates)
  277. prefix := "hue:" + b.host + ":"
  278. for id, state := range patch {
  279. if !strings.HasPrefix(id, prefix) {
  280. continue
  281. }
  282. id = id[len(prefix):]
  283. resource := resources[id]
  284. if resource == nil {
  285. continue
  286. }
  287. if !state.Empty() {
  288. desiredStates[id] = resource.FixState(state, resources)
  289. } else {
  290. delete(desiredStates, id)
  291. }
  292. }
  293. b.mu.Lock()
  294. b.desiredStates = desiredStates
  295. b.mu.Unlock()
  296. b.triggerCongruenceCheck()
  297. }
  298. func (b *Bridge) Run(ctx context.Context, bus *lucifer3.EventBus) interface{} {
  299. hwEvents, err := b.RefreshAll()
  300. if err != nil {
  301. return errors.New("failed to connect to bridge")
  302. }
  303. bus.RunEvent(events.Log{
  304. ID: "hue:" + b.host,
  305. Level: "info",
  306. Code: "hue_bridge_starting",
  307. Message: "Bridge is connecting...",
  308. })
  309. go b.makeCongruentLoop(ctx)
  310. bus.RunEvents(hwEvents)
  311. sse := b.client.SSE(ctx)
  312. step := time.NewTicker(time.Second * 30)
  313. defer step.Stop()
  314. quickStep := time.NewTicker(time.Second * 5)
  315. defer quickStep.Stop()
  316. for {
  317. select {
  318. case updates, ok := <-sse:
  319. {
  320. if !ok {
  321. return errors.New("SSE lost connection")
  322. }
  323. if len(updates) == 0 {
  324. continue
  325. }
  326. newEvents, shouldUpdate := b.ApplyPatches(
  327. updates[0].CreationTime,
  328. gentools.Flatten(gentools.Map(updates, func(update SSEUpdate) []ResourceData {
  329. return update.Data
  330. })),
  331. )
  332. bus.RunEvents(newEvents)
  333. if shouldUpdate {
  334. hwEvents, err := b.RefreshAll()
  335. if err != nil {
  336. return errors.New("failed to refresh states")
  337. }
  338. bus.RunEvents(hwEvents)
  339. }
  340. b.triggerCongruenceCheck()
  341. }
  342. case <-quickStep.C:
  343. b.mu.Lock()
  344. lastMotion := b.lastMotion
  345. lastPresence := b.lastPresence
  346. b.mu.Unlock()
  347. for id, value := range lastMotion {
  348. if lastPresence[id] {
  349. continue
  350. }
  351. since := time.Since(value)
  352. sinceMod := since % (time.Second * 30)
  353. if (since > time.Second*20) && (sinceMod >= time.Second*27 || sinceMod <= time.Second*3) {
  354. bus.RunEvent(events.MotionSensed{ID: id, SecondsSince: since.Seconds()})
  355. }
  356. }
  357. b.triggerCongruenceCheck()
  358. case <-step.C:
  359. hwEvents, err := b.RefreshAll()
  360. if err != nil {
  361. return nil
  362. }
  363. bus.RunEvents(hwEvents)
  364. b.triggerCongruenceCheck()
  365. case <-ctx.Done():
  366. {
  367. return nil
  368. }
  369. }
  370. }
  371. }
  372. func (b *Bridge) makeCongruentLoop(ctx context.Context) {
  373. hadFirst := make(map[string]bool, 16)
  374. for range b.triggerCongruenceCheckCh {
  375. // I hate that I have to do this, but the SSE is not being reliable.
  376. _, _ = b.RefreshAll()
  377. if ctx.Err() != nil {
  378. break
  379. }
  380. // Make sure this loop doesn't spam too hard
  381. rateLimit := time.After(time.Second / 15)
  382. // Take states
  383. b.mu.Lock()
  384. resources := b.resources
  385. desiredStates := b.desiredStates
  386. activeStates := b.activeStates
  387. reachable := b.reachable
  388. colorFlags := b.colorFlags
  389. b.mu.Unlock()
  390. updates := make(map[string]ResourceUpdate)
  391. for id, desired := range desiredStates {
  392. active, activeOK := activeStates[id]
  393. lightID := resources[id].ServiceID("light")
  394. if !reachable[id] || !activeOK || lightID == nil {
  395. continue
  396. }
  397. light := resources[*lightID]
  398. if light == nil {
  399. continue
  400. }
  401. updated := false
  402. update := ResourceUpdate{}
  403. // Handle power first
  404. if desired.Power != nil && active.Power != nil && (!hadFirst[*lightID] || *desired.Power != *active.Power) {
  405. update.Power = gentools.Ptr(*desired.Power)
  406. updated = true
  407. }
  408. // Only do the rest if there's power.
  409. if active.Color != nil && desired.Color != nil {
  410. ac := *active.Color
  411. dc := *desired.Color
  412. if !dc.IsKelvin() || !colorFlags[id].IsWarmWhite() {
  413. dc, _ = dc.ToXY()
  414. dc.XY = gentools.Ptr(light.Color.Gamut.Conform(*dc.XY))
  415. }
  416. if dc.XY != nil {
  417. if ac.K != nil {
  418. ac.K = gentools.Ptr(1000000 / (1000000 / *ac.K))
  419. }
  420. acXY, _ := ac.ToXY()
  421. dist := dc.XY.DistanceTo(*acXY.XY)
  422. if dist > 0.0002 || !hadFirst[*lightID] {
  423. update.ColorXY = gentools.Ptr(*dc.XY)
  424. updated = true
  425. }
  426. } else {
  427. dcMirek := 1000000 / *dc.K
  428. if dcMirek < light.ColorTemperature.MirekSchema.MirekMinimum {
  429. dcMirek = light.ColorTemperature.MirekSchema.MirekMinimum
  430. } else if dcMirek > light.ColorTemperature.MirekSchema.MirekMaximum {
  431. dcMirek = light.ColorTemperature.MirekSchema.MirekMaximum
  432. }
  433. acMirek := 0
  434. if ac.K != nil {
  435. acMirek = 1000000 / *ac.K
  436. }
  437. if acMirek != dcMirek || !hadFirst[*lightID] {
  438. update.Mirek = &dcMirek
  439. updated = true
  440. }
  441. }
  442. if active.Intensity != nil && desired.Intensity != nil || !hadFirst[*lightID] {
  443. if math.Abs(*active.Intensity-*desired.Intensity) >= 0.01 {
  444. update.Brightness = gentools.Ptr(*desired.Intensity * 100)
  445. updated = true
  446. }
  447. }
  448. }
  449. if updated {
  450. update.TransitionDuration = gentools.Ptr(time.Millisecond * 101)
  451. updates["light/"+*lightID] = update
  452. hadFirst[*lightID] = true
  453. }
  454. }
  455. if len(updates) > 0 {
  456. timeout, cancel := context.WithTimeout(ctx, time.Second*60)
  457. hadPower := false
  458. eg, ctx := errgroup.WithContext(timeout)
  459. for key := range updates {
  460. update := updates[key]
  461. split := strings.SplitN(key, "/", 2)
  462. link := ResourceLink{Kind: split[0], ID: split[1]}
  463. if update.Power != nil {
  464. hadPower = true
  465. }
  466. eg.Go(func() error {
  467. if update.Power != nil {
  468. return b.client.UpdateResource(ctx, link, ResourceUpdate{
  469. Power: update.Power,
  470. TransitionDuration: update.TransitionDuration,
  471. })
  472. } else {
  473. return b.client.UpdateResource(ctx, link, update)
  474. }
  475. })
  476. }
  477. err := eg.Wait()
  478. if err != nil {
  479. log.Println("Failed to run update", err)
  480. }
  481. if hadPower {
  482. b.triggerCongruenceCheck()
  483. }
  484. cancel()
  485. }
  486. // Wait the remaining time for the rate limit
  487. <-rateLimit
  488. }
  489. }
  490. func (b *Bridge) fullId(res ResourceData) string {
  491. id := res.ID
  492. if res.Owner != nil && res.Owner.Kind == "device" {
  493. id = res.Owner.ID
  494. }
  495. return fmt.Sprintf("hue:%s:%s", b.host, id)
  496. }
  497. func (b *Bridge) triggerCongruenceCheck() {
  498. select {
  499. case b.triggerCongruenceCheckCh <- struct{}{}:
  500. default:
  501. }
  502. }