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.

541 lines
14 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 at 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. // Eat the event if there's a pending update.
  203. select {
  204. case <-b.needsUpdate:
  205. default:
  206. }
  207. b.mu.Lock()
  208. dur := time.Millisecond * 200
  209. updates := make(map[string]ResourceUpdate)
  210. for _, device := range b.devices {
  211. resource := b.resources[device.InternalID]
  212. if lightID := resource.ServiceID("light"); lightID != nil {
  213. light := b.resources[*lightID]
  214. update := ResourceUpdate{TransitionDuration: &dur}
  215. changed := false
  216. lightsOut := light.Power != nil && !device.State.Power
  217. if !lightsOut {
  218. if light.ColorTemperature != nil && device.State.Color.IsKelvin() {
  219. mirek := 1000000 / device.State.Color.Kelvin
  220. if mirek < light.ColorTemperature.MirekSchema.MirekMinimum {
  221. mirek = light.ColorTemperature.MirekSchema.MirekMinimum
  222. }
  223. if mirek > light.ColorTemperature.MirekSchema.MirekMaximum {
  224. mirek = light.ColorTemperature.MirekSchema.MirekMaximum
  225. }
  226. if light.ColorTemperature.Mirek == nil || mirek != *light.ColorTemperature.Mirek {
  227. update.Mirek = &mirek
  228. changed = true
  229. }
  230. } else if xy, ok := device.State.Color.ToXY(); ok && light.Color != nil {
  231. xy = light.Color.Gamut.Conform(xy).Round()
  232. if xy.DistanceTo(light.Color.XY) > 0.0009 || (light.ColorTemperature != nil && light.ColorTemperature.Mirek != nil) {
  233. update.ColorXY = &xy
  234. changed = true
  235. }
  236. }
  237. if light.Dimming != nil && math.Abs(light.Dimming.Brightness/100-device.State.Intensity) > 0.02 {
  238. brightness := math.Abs(math.Min(device.State.Intensity*100, 100))
  239. update.Brightness = &brightness
  240. changed = true
  241. }
  242. }
  243. if light.Power != nil && light.Power.On != device.State.Power {
  244. update.Power = &device.State.Power
  245. if device.State.Power {
  246. brightness := math.Abs(math.Min(device.State.Intensity*100, 100))
  247. update.Brightness = &brightness
  248. }
  249. changed = true
  250. }
  251. if changed {
  252. updates["light/"+light.ID] = update
  253. }
  254. }
  255. }
  256. b.mu.Unlock()
  257. if len(updates) == 0 {
  258. return 0, nil
  259. }
  260. log.Println(fmt.Sprintf("[Bridge %d]", b.externalID), "Updating", len(updates), "services...")
  261. eg, ctx := errgroup.WithContext(ctx)
  262. for key := range updates {
  263. update := updates[key]
  264. split := strings.SplitN(key, "/", 2)
  265. link := ResourceLink{Kind: split[0], ID: split[1]}
  266. eg.Go(func() error {
  267. return b.client.UpdateResource(ctx, link, update)
  268. })
  269. }
  270. err := eg.Wait()
  271. if err != nil {
  272. return len(updates), err
  273. }
  274. if len(updates) > 0 {
  275. // Optimistically apply the updates to the states, so that the driver assumes they are set until
  276. // proven otherwise by the SSE client.
  277. b.mu.Lock()
  278. newResources := make(map[string]*ResourceData, len(b.resources))
  279. for key, value := range b.resources {
  280. newResources[key] = value
  281. }
  282. for key, update := range updates {
  283. id := strings.SplitN(key, "/", 2)[1]
  284. newResources[id] = newResources[id].WithUpdate(update)
  285. }
  286. b.resources = newResources
  287. b.mu.Unlock()
  288. }
  289. return len(updates), nil
  290. }
  291. func (b *Bridge) GenerateDevices() []models.Device {
  292. b.mu.Lock()
  293. resources := b.resources
  294. b.mu.Unlock()
  295. devices := make([]models.Device, 0, 16)
  296. for _, resource := range resources {
  297. if resource.Type != "device" || strings.HasPrefix(resource.Metadata.Archetype, "bridge") {
  298. continue
  299. }
  300. device := models.Device{
  301. InternalID: resource.ID,
  302. Name: resource.Metadata.Name,
  303. DriverProperties: map[string]interface{}{
  304. "archetype": resource.Metadata.Archetype,
  305. "name": resource.ProductData.ProductName,
  306. "product": resource.ProductData,
  307. "legacyId": resource.LegacyID,
  308. },
  309. }
  310. // Set icon
  311. if resource.ProductData.ProductName == "Hue dimmer switch" {
  312. device.Icon = "switch"
  313. } else if resource.ProductData.ProductName == "Hue motion sensor" {
  314. device.Icon = "sensor"
  315. } else {
  316. device.Icon = "lightbulb"
  317. }
  318. buttonCount := 0
  319. for _, ptr := range resource.Services {
  320. switch ptr.Kind {
  321. case "device_power":
  322. {
  323. device.DriverProperties["battery"] = resources[ptr.ID].PowerState
  324. }
  325. case "button":
  326. {
  327. buttonCount += 1
  328. }
  329. case "zigbee_connectivity":
  330. {
  331. device.DriverProperties["zigbee"] = resources[ptr.ID].Status
  332. }
  333. case "motion":
  334. {
  335. device.Capabilities = append(device.Capabilities, models.DCPresence)
  336. }
  337. case "temperature":
  338. {
  339. device.Capabilities = append(device.Capabilities, models.DCTemperatureSensor)
  340. }
  341. case "light":
  342. {
  343. light := resources[ptr.ID]
  344. if light.Power != nil {
  345. device.State.Power = light.Power.On
  346. device.Capabilities = append(device.Capabilities, models.DCPower)
  347. }
  348. if light.Dimming != nil {
  349. device.State.Intensity = light.Dimming.Brightness / 100
  350. device.Capabilities = append(device.Capabilities, models.DCIntensity)
  351. }
  352. if light.ColorTemperature != nil {
  353. if light.ColorTemperature.Mirek != nil {
  354. device.State.Color = models.ColorValue{
  355. Kelvin: int(1000000 / *light.ColorTemperature.Mirek),
  356. }
  357. }
  358. device.Capabilities = append(device.Capabilities, models.DCColorKelvin)
  359. device.DriverProperties["maxTemperature"] = 1000000 / light.ColorTemperature.MirekSchema.MirekMinimum
  360. device.DriverProperties["minTemperature"] = 1000000 / light.ColorTemperature.MirekSchema.MirekMaximum
  361. }
  362. if light.Color != nil {
  363. if device.State.Color.IsEmpty() {
  364. device.State.Color = models.ColorValue{
  365. XY: &light.Color.XY,
  366. }
  367. }
  368. device.DriverProperties["colorGamut"] = light.Color.Gamut
  369. device.DriverProperties["colorGamutType"] = light.Color.GamutType
  370. device.Capabilities = append(device.Capabilities, models.DCColorHS, models.DCColorXY)
  371. }
  372. }
  373. }
  374. }
  375. if buttonCount == 4 {
  376. device.ButtonNames = []string{"On", "DimUp", "DimDown", "Off"}
  377. } else if buttonCount == 1 {
  378. device.ButtonNames = []string{"Button"}
  379. } else {
  380. for n := 1; n <= buttonCount; n++ {
  381. device.ButtonNames = append(device.ButtonNames, fmt.Sprint("Button", n))
  382. }
  383. }
  384. devices = append(devices, device)
  385. }
  386. return devices
  387. }
  388. func (b *Bridge) Refresh(ctx context.Context, kind string) error {
  389. if kind == "device" {
  390. // Device refresh requires the full deal as services are taken for granted.
  391. return b.RefreshAll(ctx)
  392. }
  393. resources, err := b.client.Resources(ctx, kind)
  394. if err != nil {
  395. return err
  396. }
  397. b.mu.Lock()
  398. oldResources := b.resources
  399. b.mu.Unlock()
  400. newResources := make(map[string]*ResourceData, len(b.resources))
  401. for key, value := range oldResources {
  402. if value.Type != kind {
  403. newResources[key] = value
  404. }
  405. }
  406. for i := range resources {
  407. resource := resources[i]
  408. newResources[resource.ID] = &resource
  409. }
  410. b.mu.Lock()
  411. b.resources = newResources
  412. b.mu.Unlock()
  413. return nil
  414. }
  415. func (b *Bridge) RefreshAll(ctx context.Context) error {
  416. allResources, err := b.client.AllResources(ctx)
  417. if err != nil {
  418. return err
  419. }
  420. resources := make(map[string]*ResourceData, len(allResources))
  421. for i := range allResources {
  422. resource := allResources[i]
  423. resources[resource.ID] = &resource
  424. }
  425. b.mu.Lock()
  426. b.resources = resources
  427. b.mu.Unlock()
  428. return nil
  429. }
  430. func (b *Bridge) applyPatches(patches []ResourceData) {
  431. b.mu.Lock()
  432. newResources := make(map[string]*ResourceData, len(b.resources))
  433. for key, value := range b.resources {
  434. newResources[key] = value
  435. }
  436. b.mu.Unlock()
  437. for _, patch := range patches {
  438. if res := newResources[patch.ID]; res != nil {
  439. resCopy := *res
  440. if patch.Power != nil && resCopy.Power != nil {
  441. cp := *resCopy.Power
  442. resCopy.Power = &cp
  443. resCopy.Power.On = patch.Power.On
  444. }
  445. if patch.Color != nil && resCopy.Color != nil {
  446. cp := *resCopy.Color
  447. resCopy.Color = &cp
  448. resCopy.Color.XY = patch.Color.XY
  449. cp2 := *resCopy.ColorTemperature
  450. resCopy.ColorTemperature = &cp2
  451. resCopy.ColorTemperature.Mirek = nil
  452. }
  453. if patch.ColorTemperature != nil && resCopy.ColorTemperature != nil {
  454. cp := *resCopy.ColorTemperature
  455. resCopy.ColorTemperature = &cp
  456. resCopy.ColorTemperature.Mirek = patch.ColorTemperature.Mirek
  457. }
  458. if patch.Dimming != nil && resCopy.Dimming != nil {
  459. cp := *resCopy.Dimming
  460. resCopy.Dimming = &cp
  461. resCopy.Dimming.Brightness = patch.Dimming.Brightness
  462. }
  463. if patch.Dynamics != nil {
  464. resCopy.Dynamics = patch.Dynamics
  465. }
  466. if patch.Alert != nil {
  467. resCopy.Alert = patch.Alert
  468. }
  469. if patch.PowerState != nil {
  470. resCopy.PowerState = patch.PowerState
  471. }
  472. if patch.Temperature != nil {
  473. resCopy.Temperature = patch.Temperature
  474. }
  475. if patch.Status != nil {
  476. resCopy.Status = patch.Status
  477. }
  478. newResources[patch.ID] = &resCopy
  479. }
  480. }
  481. b.mu.Lock()
  482. b.resources = newResources
  483. b.mu.Unlock()
  484. }