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.

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