The main server, and probably only repository in this org.
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.

306 lines
6.4 KiB

  1. package hue
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "log"
  8. "sync"
  9. "time"
  10. "git.aiterp.net/lucifer/lucifer/internal/huecolor"
  11. "git.aiterp.net/lucifer/lucifer/light"
  12. "git.aiterp.net/lucifer/lucifer/models"
  13. gohue "github.com/collinux/gohue"
  14. "golang.org/x/sync/errgroup"
  15. )
  16. const (
  17. // FlagUseXY applies a more aggressive mode change via xy to make TradFri bulbs work.
  18. FlagUseXY = 1
  19. )
  20. type xyBri struct {
  21. XY [2]float32
  22. Bri uint8
  23. }
  24. func colorKey(light models.Light) string {
  25. return fmt.Sprintf("%s.%s.%d", light.InternalID, light.Color, light.Brightness)
  26. }
  27. // A driver is a driver for Phillips Hue lights.
  28. type driver struct {
  29. mutex sync.Mutex
  30. bridges map[int]*gohue.Bridge
  31. colors map[string]xyBri
  32. }
  33. func (d *driver) Apply(ctx context.Context, bridge models.Bridge, lights ...models.Light) error {
  34. hueBridge, err := d.getBridge(bridge)
  35. if err != nil {
  36. return err
  37. }
  38. hueLights, err := hueBridge.GetAllLights()
  39. if err != nil {
  40. return err
  41. }
  42. eg, _ := errgroup.WithContext(ctx)
  43. for _, hueLight := range hueLights {
  44. if !hueLight.State.Reachable {
  45. continue
  46. }
  47. for _, light := range lights {
  48. if hueLight.UniqueID != light.InternalID {
  49. continue
  50. }
  51. // Prevent race condition since `hueLight` changes per iteration.
  52. hl := hueLight
  53. eg.Go(func() error {
  54. if !light.On {
  55. return hl.SetState(gohue.LightState{
  56. On: false,
  57. })
  58. }
  59. x, y, bri, err := d.calcColor(light, hl)
  60. if err != nil {
  61. return err
  62. }
  63. log.Printf("Updating light (id: %d, rgb: %s, xy: [%f, %f], bri: %d)", light.ID, light.Color, x, y, bri)
  64. err = hl.SetState(gohue.LightState{
  65. On: light.On,
  66. XY: &[2]float32{float32(x), float32(y)},
  67. Bri: bri,
  68. })
  69. if err != nil {
  70. return err
  71. }
  72. hl2, err := hueBridge.GetLightByIndex(hl.Index)
  73. if err != nil {
  74. return err
  75. }
  76. d.mutex.Lock()
  77. d.colors[colorKey(light)] = xyBri{XY: hl2.State.XY, Bri: hl2.State.Bri}
  78. d.mutex.Unlock()
  79. return nil
  80. })
  81. break
  82. }
  83. }
  84. return eg.Wait()
  85. }
  86. func (d *driver) DiscoverLights(ctx context.Context, bridge models.Bridge) error {
  87. hueBridge, err := d.getBridge(bridge)
  88. if err != nil {
  89. return err
  90. }
  91. return hueBridge.FindNewLights()
  92. }
  93. func (d *driver) Lights(ctx context.Context, bridge models.Bridge) ([]models.Light, error) {
  94. hueBridge, err := d.getBridge(bridge)
  95. if err != nil {
  96. return nil, err
  97. }
  98. hueLights, err := hueBridge.GetAllLights()
  99. if err != nil {
  100. return nil, err
  101. }
  102. lights := make([]models.Light, 0, len(hueLights))
  103. for _, hueLight := range hueLights {
  104. r, g, b := huecolor.ConvertRGB(float64(hueLight.State.XY[0]), float64(hueLight.State.XY[1]), float64(hueLight.State.Bri)/255)
  105. light := models.Light{
  106. ID: -1,
  107. Name: hueLight.Name,
  108. BridgeID: bridge.ID,
  109. InternalID: hueLight.UniqueID,
  110. On: hueLight.State.On,
  111. }
  112. light.SetColorRGBf(r, g, b)
  113. light.Brightness = hueLight.State.Bri
  114. lights = append(lights, light)
  115. }
  116. return lights, nil
  117. }
  118. func (d *driver) Bridges(ctx context.Context) ([]models.Bridge, error) {
  119. panic("not implemented")
  120. }
  121. func (d *driver) Connect(ctx context.Context, bridge models.Bridge) (models.Bridge, error) {
  122. hueBridge, err := gohue.NewBridge(bridge.Addr)
  123. if err != nil {
  124. return models.Bridge{}, err
  125. }
  126. // Make 30 attempts (30 seconds)
  127. attempts := 30
  128. for attempts > 0 {
  129. key, err := hueBridge.CreateUser("Lucifer (git.aiterp.net/lucifer/lucifer)")
  130. if len(key) > 0 && err == nil {
  131. bridge.Key = []byte(key)
  132. bridge.InternalID = hueBridge.Info.Device.SerialNumber
  133. return bridge, nil
  134. }
  135. select {
  136. case <-time.After(time.Second):
  137. attempts--
  138. case <-ctx.Done():
  139. return models.Bridge{}, ctx.Err()
  140. }
  141. }
  142. return models.Bridge{}, errors.New("Bridge discovery timed out after 30 failed attempts")
  143. }
  144. func (d *driver) ChangedLights(ctx context.Context, bridge models.Bridge, lights ...models.Light) ([]models.Light, error) {
  145. hueBridge, err := d.getBridge(bridge)
  146. if err != nil {
  147. return nil, err
  148. }
  149. hueLights, err := hueBridge.GetAllLights()
  150. if err != nil {
  151. return nil, err
  152. }
  153. subset := make([]models.Light, 0, len(lights))
  154. for _, hueLight := range hueLights {
  155. for _, light := range lights {
  156. if hueLight.UniqueID != light.InternalID {
  157. continue
  158. }
  159. d.mutex.Lock()
  160. c, cOk := d.colors[colorKey(light)]
  161. d.mutex.Unlock()
  162. if !cOk || c.Bri != hueLight.State.Bri || diff(c.XY[0], hueLight.State.XY[0]) > 0.064 || diff(c.XY[1], hueLight.State.XY[1]) > 0.064 {
  163. subset = append(subset, light)
  164. }
  165. break
  166. }
  167. }
  168. return subset, nil
  169. }
  170. func (d *driver) calcColor(light models.Light, hueLight gohue.Light) (x, y float64, bri uint8, err error) {
  171. r, g, b, err := light.ColorRGBf()
  172. if err != nil {
  173. return
  174. }
  175. x, y = huecolor.ConvertXY(r, g, b)
  176. bri = light.Brightness
  177. if bri < 1 {
  178. bri = 1
  179. } else if bri > 254 {
  180. bri = 254
  181. }
  182. return
  183. }
  184. func (d *driver) getRawState(hueLight gohue.Light) (map[string]interface{}, error) {
  185. data := struct {
  186. State map[string]interface{} `json:"state"`
  187. }{
  188. State: make(map[string]interface{}, 16),
  189. }
  190. uri := fmt.Sprintf("/api/%s/lights/%d/", hueLight.Bridge.Username, hueLight.Index)
  191. _, reader, err := hueLight.Bridge.Get(uri)
  192. if err != nil {
  193. return nil, err
  194. }
  195. err = json.NewDecoder(reader).Decode(&data)
  196. return data.State, err
  197. }
  198. func (d *driver) setState(hueLight gohue.Light, key string, value interface{}) error {
  199. m := make(map[string]interface{}, 1)
  200. m[key] = value
  201. uri := fmt.Sprintf("/api/%s/lights/%d/state", hueLight.Bridge.Username, hueLight.Index)
  202. _, _, err := hueLight.Bridge.Put(uri, m)
  203. return err
  204. }
  205. func (d *driver) getBridge(bridge models.Bridge) (*gohue.Bridge, error) {
  206. d.mutex.Lock()
  207. defer d.mutex.Unlock()
  208. if hueBridge, ok := d.bridges[bridge.ID]; ok {
  209. return hueBridge, nil
  210. }
  211. hueBridge, err := gohue.NewBridge(bridge.Addr)
  212. if err != nil {
  213. return nil, err
  214. }
  215. if err := hueBridge.GetInfo(); err != nil {
  216. return nil, err
  217. }
  218. if hueBridge.Info.Device.SerialNumber != bridge.InternalID {
  219. return nil, errors.New("Serial number does not match hardware")
  220. }
  221. err = hueBridge.Login(string(bridge.Key))
  222. if err != nil {
  223. return nil, err
  224. }
  225. d.bridges[bridge.ID] = hueBridge
  226. return hueBridge, nil
  227. }
  228. func diff(a, b float32) float32 {
  229. diff := a - b
  230. if diff < 0 {
  231. return -diff
  232. }
  233. return diff
  234. }
  235. func init() {
  236. driver := &driver{
  237. bridges: make(map[int]*gohue.Bridge, 16),
  238. colors: make(map[string]xyBri, 128),
  239. }
  240. light.RegisterDriver("hue", driver)
  241. }