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.

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