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.

621 lines
16 KiB

  1. package hue2
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "git.aiterp.net/lucifer/new-server/internal/color"
  7. "git.aiterp.net/lucifer/new-server/models"
  8. "golang.org/x/sync/errgroup"
  9. "log"
  10. "math"
  11. "strings"
  12. "sync"
  13. "time"
  14. )
  15. type Bridge struct {
  16. mu sync.Mutex
  17. externalID int
  18. client *Client
  19. needsUpdate chan struct{}
  20. devices map[string]models.Device
  21. resources map[string]*ResourceData
  22. }
  23. func NewBridge(client *Client) *Bridge {
  24. return &Bridge{
  25. client: client,
  26. needsUpdate: make(chan struct{}, 4),
  27. devices: make(map[string]models.Device, 64),
  28. resources: make(map[string]*ResourceData, 256),
  29. }
  30. }
  31. func (b *Bridge) Run(ctx context.Context, eventCh chan<- models.Event) error {
  32. log.Println(fmt.Sprintf("[Bridge %d]", b.externalID), "Connecting SSE...")
  33. sse := b.client.SSE(ctx)
  34. log.Println(fmt.Sprintf("[Bridge %d]", b.externalID), "Refreshing...")
  35. err := b.RefreshAll(ctx)
  36. if err != nil {
  37. return err
  38. }
  39. log.Println(fmt.Sprintf("[Bridge %d]", b.externalID), "Running updates...")
  40. updated, err := b.MakeCongruent(ctx)
  41. if err != nil {
  42. return err
  43. }
  44. log.Println(fmt.Sprintf("[Bridge %d]", b.externalID), "Updated", updated, "hue services (startup)")
  45. lightRefreshTimer := time.NewTicker(time.Second * 5)
  46. defer lightRefreshTimer.Stop()
  47. motionTicker := time.NewTicker(time.Second * 10)
  48. defer motionTicker.Stop()
  49. absences := make(map[int]time.Time)
  50. lastPress := make(map[string]time.Time)
  51. lastUpdate := time.Now()
  52. needFull := false
  53. for {
  54. select {
  55. case <-ctx.Done():
  56. return ctx.Err()
  57. case <-lightRefreshTimer.C:
  58. if time.Since(lastUpdate) < time.Second*2 {
  59. continue
  60. }
  61. if needFull {
  62. err := b.RefreshAll(ctx)
  63. if err != nil {
  64. return err
  65. }
  66. } else {
  67. err := b.Refresh(ctx, "light")
  68. if err != nil {
  69. return err
  70. }
  71. }
  72. updated, err := b.MakeCongruent(ctx)
  73. if err != nil {
  74. return err
  75. }
  76. if updated > 0 {
  77. // Force this to cool for 30 seconds unless a manual update occurs.
  78. if updated > 10 {
  79. lastUpdate = time.Now().Add(time.Second * 15)
  80. }
  81. log.Println(fmt.Sprintf("[Bridge %d]", b.externalID), "Updated", updated, "hue services (regular check)")
  82. }
  83. case <-b.needsUpdate:
  84. lastUpdate = time.Now()
  85. updated, err := b.MakeCongruent(ctx)
  86. if err != nil {
  87. return err
  88. }
  89. if updated > 0 {
  90. log.Println(fmt.Sprintf("[Bridge %d]", b.externalID), "Updated", updated, "hue services (publish)")
  91. }
  92. case <-motionTicker.C:
  93. for id, absenceTime := range absences {
  94. seconds := int(time.Since(absenceTime).Seconds())
  95. if seconds < 10 || seconds%60 >= 10 {
  96. continue
  97. }
  98. eventCh <- models.Event{
  99. Name: models.ENSensorPresenceEnded,
  100. Payload: map[string]string{
  101. "deviceId": fmt.Sprint(id),
  102. "minutesElapsed": fmt.Sprint(seconds / 60),
  103. "secondsElapsed": fmt.Sprint(seconds),
  104. "lastUpdated": fmt.Sprint(absenceTime.Unix()),
  105. },
  106. }
  107. }
  108. case data, ok := <-sse:
  109. if !ok {
  110. return errors.New("SSE Disconnected")
  111. }
  112. b.applyPatches(data.Data)
  113. updated, err := b.MakeCongruent(ctx)
  114. if err != nil {
  115. return err
  116. }
  117. if updated > 0 {
  118. log.Println(fmt.Sprintf("[Bridge %d]", b.externalID), "Updated", updated, "hue services (SSE)")
  119. }
  120. for _, patch := range data.Data {
  121. if patch.Owner == nil {
  122. continue
  123. }
  124. b.mu.Lock()
  125. if b.resources[patch.Owner.ID] == nil {
  126. needFull = true
  127. }
  128. b.mu.Unlock()
  129. device, deviceOK := b.devices[patch.Owner.ID]
  130. if !deviceOK || device.ID == 0 {
  131. continue
  132. }
  133. if patch.Button != nil {
  134. valid := false
  135. if patch.Button.LastEvent == "initial_press" || patch.Button.LastEvent == "repeat" {
  136. valid = true
  137. } else if patch.Button.LastEvent == "long_release" {
  138. valid = false
  139. } else {
  140. valid = data.CreationTime.Sub(lastPress[patch.ID]) >= time.Second*2
  141. }
  142. if valid {
  143. lastPress[patch.ID] = data.CreationTime
  144. b.mu.Lock()
  145. owner := b.resources[patch.Owner.ID]
  146. b.mu.Unlock()
  147. if owner != nil {
  148. index := owner.ServiceIndex("button", patch.ID)
  149. if index != -1 {
  150. eventCh <- models.Event{
  151. Name: models.ENButtonPressed,
  152. Payload: map[string]string{
  153. "deviceId": fmt.Sprint(device.ID),
  154. "hueButtonEvent": patch.Button.LastEvent,
  155. "buttonIndex": fmt.Sprint(index),
  156. "buttonName": device.ButtonNames[index],
  157. },
  158. }
  159. }
  160. }
  161. }
  162. }
  163. if patch.Temperature != nil && patch.Temperature.Valid {
  164. eventCh <- models.Event{
  165. Name: models.ENSensorTemperature,
  166. Payload: map[string]string{
  167. "deviceId": fmt.Sprint(device.ID),
  168. "deviceInternalId": patch.Owner.ID,
  169. "temperature": fmt.Sprint(patch.Temperature.Temperature),
  170. "lastUpdated": fmt.Sprint(data.CreationTime.Unix()),
  171. },
  172. }
  173. }
  174. if patch.Motion != nil && patch.Motion.Valid {
  175. if patch.Motion.Motion {
  176. eventCh <- models.Event{
  177. Name: models.ENSensorPresenceStarted,
  178. Payload: map[string]string{
  179. "deviceId": fmt.Sprint(device.ID),
  180. "deviceInternalId": patch.Owner.ID,
  181. },
  182. }
  183. delete(absences, device.ID)
  184. } else {
  185. eventCh <- models.Event{
  186. Name: models.ENSensorPresenceEnded,
  187. Payload: map[string]string{
  188. "deviceId": fmt.Sprint(device.ID),
  189. "deviceInternalId": device.InternalID,
  190. "minutesElapsed": "0",
  191. "secondsElapsed": "0",
  192. "lastUpdated": fmt.Sprint(data.CreationTime.Unix()),
  193. },
  194. }
  195. absences[device.ID] = data.CreationTime
  196. }
  197. }
  198. }
  199. }
  200. }
  201. }
  202. func (b *Bridge) Update(devices ...models.Device) {
  203. b.mu.Lock()
  204. for _, device := range devices {
  205. b.devices[device.InternalID] = device
  206. }
  207. b.mu.Unlock()
  208. select {
  209. case b.needsUpdate <- struct{}{}:
  210. default:
  211. }
  212. }
  213. func (b *Bridge) MakeCongruent(ctx context.Context) (int, error) {
  214. // Exhaust the channel to avoid more updates.
  215. exhausted := false
  216. for !exhausted {
  217. select {
  218. case <-b.needsUpdate:
  219. default:
  220. exhausted = true
  221. }
  222. }
  223. b.mu.Lock()
  224. dur := time.Millisecond * 100
  225. updates := make(map[string]ResourceUpdate)
  226. for _, device := range b.devices {
  227. resource := b.resources[device.InternalID]
  228. // Update device
  229. if resource.Metadata.Name != device.Name {
  230. name := device.Name
  231. updates["device/"+resource.ID] = ResourceUpdate{
  232. Name: &name,
  233. }
  234. }
  235. // Update light
  236. if lightID := resource.ServiceID("light"); lightID != nil {
  237. light := b.resources[*lightID]
  238. update := ResourceUpdate{TransitionDuration: &dur}
  239. changed := false
  240. lightsOut := light.Power != nil && !device.State.Power
  241. if !lightsOut {
  242. if light.ColorTemperature != nil && device.State.Color.IsKelvin() {
  243. mirek := 1000000 / *device.State.Color.K
  244. if mirek < light.ColorTemperature.MirekSchema.MirekMinimum {
  245. mirek = light.ColorTemperature.MirekSchema.MirekMinimum
  246. }
  247. if mirek > light.ColorTemperature.MirekSchema.MirekMaximum {
  248. mirek = light.ColorTemperature.MirekSchema.MirekMaximum
  249. }
  250. if light.ColorTemperature.Mirek == nil || mirek != *light.ColorTemperature.Mirek {
  251. update.Mirek = &mirek
  252. changed = true
  253. }
  254. } else if xyColor, ok := device.State.Color.ToXY(); ok && light.Color != nil {
  255. xy := light.Color.Gamut.Conform(*xyColor.XY).Round()
  256. if xy.DistanceTo(light.Color.XY) > 0.0009 || (light.ColorTemperature != nil && light.ColorTemperature.Mirek != nil) {
  257. update.ColorXY = &xy
  258. changed = true
  259. }
  260. }
  261. if light.Dimming != nil && math.Abs(light.Dimming.Brightness/100-device.State.Intensity) > 0.02 {
  262. brightness := math.Abs(math.Min(device.State.Intensity*100, 100))
  263. update.Brightness = &brightness
  264. changed = true
  265. }
  266. }
  267. if light.Power != nil && light.Power.On != device.State.Power {
  268. update.Power = &device.State.Power
  269. if device.State.Power {
  270. brightness := math.Abs(math.Min(device.State.Intensity*100, 100))
  271. update.Brightness = &brightness
  272. }
  273. changed = true
  274. }
  275. if changed {
  276. updates["light/"+light.ID] = update
  277. }
  278. }
  279. }
  280. if len(updates) > 0 {
  281. // Optimistically apply the updates to the states, so that the driver assumes they are set until
  282. // proven otherwise by the SSE client.
  283. newResources := make(map[string]*ResourceData, len(b.resources))
  284. for key, value := range b.resources {
  285. newResources[key] = value
  286. }
  287. for key, update := range updates {
  288. id := strings.SplitN(key, "/", 2)[1]
  289. newResources[id] = newResources[id].WithUpdate(update)
  290. }
  291. b.resources = newResources
  292. }
  293. b.mu.Unlock()
  294. if len(updates) == 0 {
  295. return 0, nil
  296. }
  297. eg, ctx := errgroup.WithContext(ctx)
  298. for key := range updates {
  299. update := updates[key]
  300. split := strings.SplitN(key, "/", 2)
  301. link := ResourceLink{Kind: split[0], ID: split[1]}
  302. eg.Go(func() error {
  303. return b.client.UpdateResource(ctx, link, update)
  304. })
  305. }
  306. err := eg.Wait()
  307. if err != nil {
  308. // Try to restore light states.
  309. _ = b.Refresh(ctx, "light")
  310. return len(updates), err
  311. }
  312. return len(updates), nil
  313. }
  314. func (b *Bridge) GenerateDevices() []models.Device {
  315. b.mu.Lock()
  316. resources := b.resources
  317. b.mu.Unlock()
  318. devices := make([]models.Device, 0, 16)
  319. for _, resource := range resources {
  320. if resource.Type != "device" || strings.HasPrefix(resource.Metadata.Archetype, "bridge") {
  321. continue
  322. }
  323. device := models.Device{
  324. BridgeID: b.externalID,
  325. InternalID: resource.ID,
  326. Name: resource.Metadata.Name,
  327. DriverProperties: map[string]interface{}{
  328. "archetype": resource.Metadata.Archetype,
  329. "name": resource.ProductData.ProductName,
  330. "product": resource.ProductData,
  331. "legacyId": resource.LegacyID,
  332. },
  333. }
  334. // Set icon
  335. if resource.ProductData.ProductName == "Hue dimmer switch" {
  336. device.Icon = "switch"
  337. } else if resource.ProductData.ProductName == "Hue motion sensor" {
  338. device.Icon = "sensor"
  339. } else {
  340. device.Icon = "lightbulb"
  341. }
  342. buttonCount := 0
  343. for _, ptr := range resource.Services {
  344. switch ptr.Kind {
  345. case "device_power":
  346. {
  347. device.DriverProperties["battery"] = resources[ptr.ID].PowerState
  348. }
  349. case "button":
  350. {
  351. buttonCount += 1
  352. }
  353. case "zigbee_connectivity":
  354. {
  355. device.DriverProperties["zigbee"] = resources[ptr.ID].Status
  356. }
  357. case "motion":
  358. {
  359. device.Capabilities = append(device.Capabilities, models.DCPresence)
  360. }
  361. case "temperature":
  362. {
  363. device.Capabilities = append(device.Capabilities, models.DCTemperatureSensor)
  364. }
  365. case "light":
  366. {
  367. light := resources[ptr.ID]
  368. if light.Power != nil {
  369. device.State.Power = light.Power.On
  370. device.Capabilities = append(device.Capabilities, models.DCPower)
  371. }
  372. if light.Dimming != nil {
  373. device.State.Intensity = light.Dimming.Brightness / 100
  374. device.Capabilities = append(device.Capabilities, models.DCIntensity)
  375. }
  376. if light.ColorTemperature != nil {
  377. if light.ColorTemperature.Mirek != nil {
  378. device.State.Color.SetK(1000000 / *light.ColorTemperature.Mirek)
  379. }
  380. device.Capabilities = append(device.Capabilities, models.DCColorKelvin)
  381. device.DriverProperties["maxTemperature"] = 1000000 / light.ColorTemperature.MirekSchema.MirekMinimum
  382. device.DriverProperties["minTemperature"] = 1000000 / light.ColorTemperature.MirekSchema.MirekMaximum
  383. }
  384. if light.Color != nil {
  385. if device.State.Color.IsEmpty() {
  386. device.State.Color = color.Color{
  387. XY: &light.Color.XY,
  388. }
  389. }
  390. device.DriverProperties["colorGamut"] = light.Color.Gamut
  391. device.DriverProperties["colorGamutType"] = light.Color.GamutType
  392. device.Capabilities = append(device.Capabilities, models.DCColorHS, models.DCColorXY)
  393. }
  394. }
  395. }
  396. }
  397. if buttonCount == 4 {
  398. device.ButtonNames = []string{"On", "DimUp", "DimDown", "Off"}
  399. } else if buttonCount == 1 {
  400. device.ButtonNames = []string{"Button"}
  401. } else {
  402. for n := 1; n <= buttonCount; n++ {
  403. device.ButtonNames = append(device.ButtonNames, fmt.Sprint("Button", n))
  404. }
  405. }
  406. devices = append(devices, device)
  407. }
  408. return devices
  409. }
  410. func (b *Bridge) Refresh(ctx context.Context, kind string) error {
  411. if kind == "device" {
  412. // Device refresh requires the full deal as services are taken for granted.
  413. return b.RefreshAll(ctx)
  414. }
  415. resources, err := b.client.Resources(ctx, kind)
  416. if err != nil {
  417. return err
  418. }
  419. b.mu.Lock()
  420. oldResources := b.resources
  421. b.mu.Unlock()
  422. newResources := make(map[string]*ResourceData, len(b.resources))
  423. for key, value := range oldResources {
  424. if value.Type != kind {
  425. newResources[key] = value
  426. }
  427. }
  428. for i := range resources {
  429. resource := resources[i]
  430. newResources[resource.ID] = &resource
  431. }
  432. b.mu.Lock()
  433. b.resources = newResources
  434. b.mu.Unlock()
  435. return nil
  436. }
  437. func (b *Bridge) RefreshAll(ctx context.Context) error {
  438. allResources, err := b.client.AllResources(ctx)
  439. if err != nil {
  440. return err
  441. }
  442. resources := make(map[string]*ResourceData, len(allResources))
  443. for i := range allResources {
  444. resource := allResources[i]
  445. resources[resource.ID] = &resource
  446. }
  447. b.mu.Lock()
  448. b.resources = resources
  449. b.mu.Unlock()
  450. return nil
  451. }
  452. func (b *Bridge) applyPatches(patches []ResourceData) {
  453. b.mu.Lock()
  454. newResources := make(map[string]*ResourceData, len(b.resources))
  455. for key, value := range b.resources {
  456. newResources[key] = value
  457. }
  458. b.mu.Unlock()
  459. for _, patch := range patches {
  460. if res := newResources[patch.ID]; res != nil {
  461. resCopy := *res
  462. if patch.Power != nil && resCopy.Power != nil {
  463. cp := *resCopy.Power
  464. resCopy.Power = &cp
  465. resCopy.Power.On = patch.Power.On
  466. }
  467. if patch.Color != nil && resCopy.Color != nil {
  468. cp := *resCopy.Color
  469. resCopy.Color = &cp
  470. resCopy.Color.XY = patch.Color.XY
  471. if resCopy.ColorTemperature != nil {
  472. cp2 := *resCopy.ColorTemperature
  473. resCopy.ColorTemperature = &cp2
  474. resCopy.ColorTemperature.Mirek = nil
  475. }
  476. }
  477. if patch.ColorTemperature != nil && resCopy.ColorTemperature != nil {
  478. cp := *resCopy.ColorTemperature
  479. resCopy.ColorTemperature = &cp
  480. resCopy.ColorTemperature.Mirek = patch.ColorTemperature.Mirek
  481. }
  482. if patch.Dimming != nil && resCopy.Dimming != nil {
  483. cp := *resCopy.Dimming
  484. resCopy.Dimming = &cp
  485. resCopy.Dimming.Brightness = patch.Dimming.Brightness
  486. }
  487. if patch.Dynamics != nil {
  488. resCopy.Dynamics = patch.Dynamics
  489. }
  490. if patch.Alert != nil {
  491. resCopy.Alert = patch.Alert
  492. }
  493. if patch.PowerState != nil {
  494. resCopy.PowerState = patch.PowerState
  495. }
  496. if patch.Temperature != nil {
  497. resCopy.Temperature = patch.Temperature
  498. }
  499. if patch.Status != nil {
  500. resCopy.Status = patch.Status
  501. }
  502. resCopy.Metadata.Name = patch.Metadata.Name
  503. newResources[patch.ID] = &resCopy
  504. }
  505. }
  506. b.mu.Lock()
  507. b.resources = newResources
  508. b.mu.Unlock()
  509. }
  510. func (b *Bridge) SearchDevices(ctx context.Context, timeout time.Duration) ([]models.Device, error) {
  511. // Record the current state.
  512. b.mu.Lock()
  513. before := b.resources
  514. b.mu.Unlock()
  515. // Spend half the time waiting for devices
  516. // TODO: Wait for v2 endpoint
  517. err := b.client.LegacyDiscover(ctx, "sensors")
  518. if err != nil {
  519. return nil, err
  520. }
  521. select {
  522. case <-time.After(timeout / 1):
  523. case <-ctx.Done():
  524. return nil, ctx.Err()
  525. }
  526. // Spend half the time waiting for lights
  527. // TODO: Wait for v2 endpoint
  528. err = b.client.LegacyDiscover(ctx, "lights")
  529. if err != nil {
  530. return nil, err
  531. }
  532. select {
  533. case <-time.After(timeout / 1):
  534. case <-ctx.Done():
  535. return nil, ctx.Err()
  536. }
  537. // Perform a full refresh.
  538. err = b.RefreshAll(ctx)
  539. if err != nil {
  540. return nil, err
  541. }
  542. // Check for new devices
  543. devices := b.GenerateDevices()
  544. newDevices := make([]models.Device, 0)
  545. for _, device := range devices {
  546. if before[device.InternalID] == nil {
  547. newDevices = append(newDevices, device)
  548. }
  549. }
  550. // Return said devices.
  551. return newDevices, nil
  552. }