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.

639 lines
16 KiB

1 year ago
1 year ago
  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. if resource == nil {
  229. continue
  230. }
  231. // Update device
  232. if resource.Metadata.Name != device.Name {
  233. name := device.Name
  234. updates["device/"+resource.ID] = ResourceUpdate{
  235. Name: &name,
  236. }
  237. }
  238. // Update light
  239. if lightID := resource.ServiceID("light"); lightID != nil {
  240. light := b.resources[*lightID]
  241. update := ResourceUpdate{TransitionDuration: &dur}
  242. changed := false
  243. lightsOut := light.Power != nil && !device.State.Power
  244. if !lightsOut {
  245. if light.ColorTemperature != nil && device.State.Color.IsKelvin() {
  246. mirek := 1000000 / *device.State.Color.K
  247. if mirek < light.ColorTemperature.MirekSchema.MirekMinimum {
  248. mirek = light.ColorTemperature.MirekSchema.MirekMinimum
  249. }
  250. if mirek > light.ColorTemperature.MirekSchema.MirekMaximum {
  251. mirek = light.ColorTemperature.MirekSchema.MirekMaximum
  252. }
  253. if light.ColorTemperature.Mirek == nil || mirek != *light.ColorTemperature.Mirek {
  254. update.Mirek = &mirek
  255. changed = true
  256. }
  257. } else if xyColor, ok := device.State.Color.ToXY(); ok && light.Color != nil {
  258. xy := light.Color.Gamut.Conform(*xyColor.XY).Round()
  259. if xy.DistanceTo(light.Color.XY) > 0.0009 || (light.ColorTemperature != nil && light.ColorTemperature.Mirek != nil) {
  260. update.ColorXY = &xy
  261. changed = true
  262. }
  263. }
  264. if light.Dimming != nil && math.Abs(light.Dimming.Brightness/100-device.State.Intensity) > 0.02 {
  265. brightness := math.Abs(math.Min(device.State.Intensity*100, 100))
  266. update.Brightness = &brightness
  267. changed = true
  268. }
  269. }
  270. if light.Power != nil && light.Power.On != device.State.Power {
  271. update.Power = &device.State.Power
  272. if device.State.Power {
  273. brightness := math.Abs(math.Min(device.State.Intensity*100, 100))
  274. update.Brightness = &brightness
  275. }
  276. changed = true
  277. }
  278. if changed {
  279. updates["light/"+light.ID] = update
  280. }
  281. }
  282. }
  283. if len(updates) > 0 {
  284. // Optimistically apply the updates to the states, so that the driver assumes they are set until
  285. // proven otherwise by the SSE client.
  286. newResources := make(map[string]*ResourceData, len(b.resources))
  287. for key, value := range b.resources {
  288. newResources[key] = value
  289. }
  290. for key, update := range updates {
  291. id := strings.SplitN(key, "/", 2)[1]
  292. newResources[id] = newResources[id].WithUpdate(update)
  293. }
  294. b.resources = newResources
  295. }
  296. b.mu.Unlock()
  297. if len(updates) == 0 {
  298. return 0, nil
  299. }
  300. eg, ctx := errgroup.WithContext(ctx)
  301. for key := range updates {
  302. update := updates[key]
  303. split := strings.SplitN(key, "/", 2)
  304. link := ResourceLink{Kind: split[0], ID: split[1]}
  305. eg.Go(func() error {
  306. return b.client.UpdateResource(ctx, link, update)
  307. })
  308. }
  309. err := eg.Wait()
  310. if err != nil {
  311. // Try to restore light states.
  312. _ = b.Refresh(ctx, "light")
  313. return len(updates), err
  314. }
  315. return len(updates), nil
  316. }
  317. func (b *Bridge) GenerateDevices() []models.Device {
  318. b.mu.Lock()
  319. resources := b.resources
  320. b.mu.Unlock()
  321. devices := make([]models.Device, 0, 16)
  322. for _, resource := range resources {
  323. if resource.Type != "device" || strings.HasPrefix(resource.Metadata.Archetype, "bridge") {
  324. continue
  325. }
  326. device := models.Device{
  327. BridgeID: b.externalID,
  328. InternalID: resource.ID,
  329. Name: resource.Metadata.Name,
  330. DriverProperties: map[string]interface{}{
  331. "archetype": resource.Metadata.Archetype,
  332. "name": resource.ProductData.ProductName,
  333. "product": resource.ProductData,
  334. "legacyId": resource.LegacyID,
  335. },
  336. }
  337. // Set icon
  338. if resource.ProductData.ProductName == "Hue dimmer switch" {
  339. device.Icon = "switch"
  340. } else if resource.ProductData.ProductName == "Hue motion sensor" {
  341. device.Icon = "sensor"
  342. } else {
  343. device.Icon = "lightbulb"
  344. }
  345. buttonCount := 0
  346. for _, ptr := range resource.Services {
  347. switch ptr.Kind {
  348. case "device_power":
  349. {
  350. device.DriverProperties["battery"] = resources[ptr.ID].PowerState
  351. }
  352. case "button":
  353. {
  354. buttonCount += 1
  355. }
  356. case "zigbee_connectivity":
  357. {
  358. device.DriverProperties["zigbee"] = resources[ptr.ID].Status
  359. }
  360. case "motion":
  361. {
  362. device.Capabilities = append(device.Capabilities, models.DCPresence)
  363. }
  364. case "temperature":
  365. {
  366. device.Capabilities = append(device.Capabilities, models.DCTemperatureSensor)
  367. }
  368. case "light":
  369. {
  370. light := resources[ptr.ID]
  371. if light.Power != nil {
  372. device.State.Power = light.Power.On
  373. device.Capabilities = append(device.Capabilities, models.DCPower)
  374. }
  375. if light.Dimming != nil {
  376. device.State.Intensity = light.Dimming.Brightness / 100
  377. device.Capabilities = append(device.Capabilities, models.DCIntensity)
  378. }
  379. if light.ColorTemperature != nil {
  380. if light.ColorTemperature.Mirek != nil {
  381. device.State.Color.SetK(1000000 / *light.ColorTemperature.Mirek)
  382. }
  383. device.Capabilities = append(device.Capabilities, models.DCColorKelvin)
  384. device.DriverProperties["maxTemperature"] = 1000000 / light.ColorTemperature.MirekSchema.MirekMinimum
  385. device.DriverProperties["minTemperature"] = 1000000 / light.ColorTemperature.MirekSchema.MirekMaximum
  386. }
  387. if light.Color != nil {
  388. if device.State.Color.IsEmpty() {
  389. device.State.Color = color.Color{
  390. XY: &light.Color.XY,
  391. }
  392. }
  393. device.DriverProperties["colorGamut"] = light.Color.Gamut
  394. device.DriverProperties["colorGamutType"] = light.Color.GamutType
  395. device.Capabilities = append(device.Capabilities, models.DCColorHS, models.DCColorXY)
  396. }
  397. }
  398. }
  399. }
  400. if buttonCount == 4 {
  401. device.ButtonNames = []string{"On", "DimUp", "DimDown", "Off"}
  402. } else if buttonCount == 1 {
  403. device.ButtonNames = []string{"Button"}
  404. } else {
  405. for n := 1; n <= buttonCount; n++ {
  406. device.ButtonNames = append(device.ButtonNames, fmt.Sprint("Button", n))
  407. }
  408. }
  409. devices = append(devices, device)
  410. }
  411. return devices
  412. }
  413. func (b *Bridge) Refresh(ctx context.Context, kind string) error {
  414. if kind == "device" {
  415. // Device refresh requires the full deal as services are taken for granted.
  416. return b.RefreshAll(ctx)
  417. }
  418. resources, err := b.client.Resources(ctx, kind)
  419. if err != nil {
  420. return err
  421. }
  422. b.mu.Lock()
  423. oldResources := b.resources
  424. b.mu.Unlock()
  425. newResources := make(map[string]*ResourceData, len(b.resources))
  426. for key, value := range oldResources {
  427. if value.Type != kind {
  428. newResources[key] = value
  429. }
  430. }
  431. for i := range resources {
  432. resource := resources[i]
  433. newResources[resource.ID] = &resource
  434. }
  435. b.mu.Lock()
  436. b.resources = newResources
  437. b.mu.Unlock()
  438. return nil
  439. }
  440. func (b *Bridge) RefreshAll(ctx context.Context) error {
  441. allResources, err := b.client.AllResources(ctx)
  442. if err != nil {
  443. return err
  444. }
  445. resources := make(map[string]*ResourceData, len(allResources))
  446. for i := range allResources {
  447. resource := allResources[i]
  448. resources[resource.ID] = &resource
  449. }
  450. b.mu.Lock()
  451. b.resources = resources
  452. b.mu.Unlock()
  453. return nil
  454. }
  455. func (b *Bridge) applyPatches(patches []ResourceData) {
  456. b.mu.Lock()
  457. newResources := make(map[string]*ResourceData, len(b.resources))
  458. for key, value := range b.resources {
  459. newResources[key] = value
  460. }
  461. b.mu.Unlock()
  462. for _, patch := range patches {
  463. if res := newResources[patch.ID]; res != nil {
  464. resCopy := *res
  465. if patch.Power != nil && resCopy.Power != nil {
  466. cp := *resCopy.Power
  467. resCopy.Power = &cp
  468. resCopy.Power.On = patch.Power.On
  469. }
  470. if patch.Color != nil && resCopy.Color != nil {
  471. cp := *resCopy.Color
  472. resCopy.Color = &cp
  473. resCopy.Color.XY = patch.Color.XY
  474. if resCopy.ColorTemperature != nil {
  475. cp2 := *resCopy.ColorTemperature
  476. resCopy.ColorTemperature = &cp2
  477. resCopy.ColorTemperature.Mirek = nil
  478. }
  479. }
  480. if patch.ColorTemperature != nil && resCopy.ColorTemperature != nil {
  481. cp := *resCopy.ColorTemperature
  482. resCopy.ColorTemperature = &cp
  483. resCopy.ColorTemperature.Mirek = patch.ColorTemperature.Mirek
  484. }
  485. if patch.Dimming != nil && resCopy.Dimming != nil {
  486. cp := *resCopy.Dimming
  487. resCopy.Dimming = &cp
  488. resCopy.Dimming.Brightness = patch.Dimming.Brightness
  489. }
  490. if patch.Dynamics != nil {
  491. resCopy.Dynamics = patch.Dynamics
  492. }
  493. if patch.Alert != nil {
  494. resCopy.Alert = patch.Alert
  495. }
  496. if patch.PowerState != nil {
  497. resCopy.PowerState = patch.PowerState
  498. }
  499. if patch.Temperature != nil {
  500. resCopy.Temperature = patch.Temperature
  501. }
  502. if patch.Status != nil {
  503. resCopy.Status = patch.Status
  504. }
  505. resCopy.Metadata.Name = patch.Metadata.Name
  506. newResources[patch.ID] = &resCopy
  507. }
  508. }
  509. b.mu.Lock()
  510. b.resources = newResources
  511. b.mu.Unlock()
  512. }
  513. func (b *Bridge) SearchDevices(ctx context.Context, timeout time.Duration) ([]models.Device, error) {
  514. // Record the current state.
  515. b.mu.Lock()
  516. before := b.resources
  517. b.mu.Unlock()
  518. // Spend half the time waiting for devices
  519. // TODO: Wait for v2 endpoint
  520. err := b.client.LegacyDiscover(ctx, "sensors")
  521. if err != nil {
  522. return nil, err
  523. }
  524. select {
  525. case <-time.After(timeout / 1):
  526. case <-ctx.Done():
  527. return nil, ctx.Err()
  528. }
  529. // Spend half the time waiting for lights
  530. // TODO: Wait for v2 endpoint
  531. err = b.client.LegacyDiscover(ctx, "lights")
  532. if err != nil {
  533. return nil, err
  534. }
  535. select {
  536. case <-time.After(timeout / 1):
  537. case <-ctx.Done():
  538. return nil, ctx.Err()
  539. }
  540. // Perform a full refresh.
  541. err = b.RefreshAll(ctx)
  542. if err != nil {
  543. return nil, err
  544. }
  545. // Check for new devices
  546. devices := b.GenerateDevices()
  547. newDevices := make([]models.Device, 0)
  548. for _, device := range devices {
  549. if before[device.InternalID] == nil {
  550. newDevices = append(newDevices, device)
  551. }
  552. }
  553. // Return said devices.
  554. return newDevices, nil
  555. }
  556. func (b *Bridge) Forget(ctx context.Context, device models.Device) error {
  557. b.mu.Lock()
  558. resource := b.resources[device.InternalID]
  559. b.mu.Unlock()
  560. if resource == nil {
  561. return errors.New("resource not found")
  562. }
  563. return b.client.DeleteResource(ctx, ResourceLink{
  564. ID: device.InternalID,
  565. Kind: "device",
  566. })
  567. }