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.

620 lines
16 KiB

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