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.

471 lines
11 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/httperr"
  11. "git.aiterp.net/lucifer/lucifer/internal/huecolor"
  12. "git.aiterp.net/lucifer/lucifer/light"
  13. "git.aiterp.net/lucifer/lucifer/models"
  14. gohue "github.com/collinux/gohue"
  15. "golang.org/x/sync/errgroup"
  16. )
  17. const (
  18. // FlagUseXY applies a more aggressive mode change via xy to make TradFri bulbs work.
  19. FlagUseXY = 1
  20. )
  21. type xyBri struct {
  22. XY [2]float32
  23. Bri uint8
  24. }
  25. func colorKey(light models.Light) string {
  26. return fmt.Sprintf("%s.%s.%d", light.InternalID, light.Color, light.Brightness)
  27. }
  28. // A driver is a driver for Phillips Hue lights.
  29. type driver struct {
  30. mutex sync.Mutex
  31. bridges map[int]*gohue.Bridge
  32. colors map[string]xyBri
  33. }
  34. func (d *driver) Apply(ctx context.Context, bridge models.Bridge, lights ...models.Light) error {
  35. hueBridge, err := d.getBridge(bridge)
  36. if err != nil {
  37. return err
  38. }
  39. hueLights, err := hueBridge.GetAllLights()
  40. if err != nil {
  41. return err
  42. }
  43. eg, _ := errgroup.WithContext(ctx)
  44. for _, hueLight := range hueLights {
  45. if !hueLight.State.Reachable {
  46. continue
  47. }
  48. for _, light := range lights {
  49. if hueLight.UniqueID != light.InternalID {
  50. continue
  51. }
  52. hl := hueLight // `hueLight` will change while the gorouting below still needs it.
  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) PollButton(ctx context.Context, bridge models.Bridge, button models.Button) (<-chan models.ButtonEvent, error) {
  94. hueBridge, err := d.getBridge(bridge)
  95. if err != nil {
  96. return nil, err
  97. }
  98. channel := make(chan models.ButtonEvent, 60)
  99. go func() {
  100. fastTicker := time.NewTicker(time.Second / 30)
  101. slowTicker := time.NewTicker(time.Second / 3)
  102. ticker := slowTicker
  103. checkTicker := time.NewTicker(time.Second * 5)
  104. gotPress := make([]bool, button.NumButtons+1)
  105. lastEventTime := time.Now()
  106. lastEvent := uint16(0)
  107. lastButton := 0
  108. for {
  109. select {
  110. case <-ticker.C:
  111. {
  112. sensor, err := hueBridge.GetSensorByIndex(button.InternalIndex)
  113. if err != nil {
  114. log.Println("Sensor poll error:", err)
  115. continue
  116. }
  117. if sensor.State.LastUpdated.Time == nil || sensor.State.LastUpdated.Before(lastEventTime) {
  118. continue
  119. }
  120. if sensor.State.LastUpdated.Equal(lastEventTime) && lastEvent == sensor.State.ButtonEvent {
  121. continue
  122. }
  123. if ticker != fastTicker {
  124. ticker = fastTicker
  125. }
  126. buttonIndex := int(sensor.State.ButtonEvent) / 1000
  127. buttonEvent := int(sensor.State.ButtonEvent) % 1000
  128. // Slip in a press event if there's a release not associated with a press
  129. if buttonEvent >= 2 {
  130. if !gotPress[buttonIndex] {
  131. channel <- models.ButtonEvent{Index: buttonIndex, Kind: models.ButtonEventKindPress}
  132. }
  133. }
  134. // Slip in a release event if the last button was pressed but the release got lost betwen polls
  135. if lastButton != 0 && buttonIndex != lastButton && gotPress[lastButton] {
  136. channel <- models.ButtonEvent{Index: lastButton, Kind: models.ButtonEventKindRelease}
  137. }
  138. lastEvent = sensor.State.ButtonEvent
  139. lastEventTime = *sensor.State.LastUpdated.Time
  140. lastButton = buttonIndex
  141. switch buttonEvent {
  142. case 0:
  143. // Slip in a release event if this was a consecutive press but the release got lost betwen polls
  144. if lastButton == buttonIndex && gotPress[lastButton] {
  145. channel <- models.ButtonEvent{Index: lastButton, Kind: models.ButtonEventKindRelease}
  146. }
  147. gotPress[buttonIndex] = true
  148. channel <- models.ButtonEvent{Index: buttonIndex, Kind: models.ButtonEventKindPress}
  149. case 1:
  150. gotPress[buttonIndex] = true
  151. channel <- models.ButtonEvent{Index: buttonIndex, Kind: models.ButtonEventKindRepeat}
  152. case 2, 3:
  153. gotPress[buttonIndex] = false
  154. channel <- models.ButtonEvent{Index: buttonIndex, Kind: models.ButtonEventKindRelease}
  155. }
  156. }
  157. case <-checkTicker.C:
  158. {
  159. if ticker != slowTicker && time.Since(lastEventTime) > time.Second*3 {
  160. ticker = slowTicker
  161. }
  162. }
  163. case <-ctx.Done():
  164. {
  165. ticker.Stop()
  166. close(channel)
  167. return
  168. }
  169. }
  170. }
  171. }()
  172. return channel, nil
  173. }
  174. func (d *driver) Buttons(ctx context.Context, bridge models.Bridge) ([]models.Button, error) {
  175. hueBridge, err := d.getBridge(bridge)
  176. if err != nil {
  177. return nil, err
  178. }
  179. sensors, err := hueBridge.GetAllSensors()
  180. if err != nil {
  181. return nil, err
  182. }
  183. buttons := make([]models.Button, 0, len(sensors))
  184. for _, sensor := range sensors {
  185. if sensor.Type == "ZLLSwitch" {
  186. buttons = append(buttons, models.Button{
  187. ID: -1,
  188. BridgeID: bridge.ID,
  189. InternalIndex: sensor.Index,
  190. InternalID: sensor.UniqueID,
  191. Name: sensor.Name,
  192. Kind: sensor.Type,
  193. NumButtons: 4,
  194. TargetGroupID: -1,
  195. })
  196. }
  197. }
  198. return buttons, nil
  199. }
  200. func (d *driver) Lights(ctx context.Context, bridge models.Bridge) ([]models.Light, error) {
  201. hueBridge, err := d.getBridge(bridge)
  202. if err != nil {
  203. return nil, err
  204. }
  205. hueLights, err := hueBridge.GetAllLights()
  206. if err != nil {
  207. return nil, err
  208. }
  209. lights := make([]models.Light, 0, len(hueLights))
  210. for _, hueLight := range hueLights {
  211. r, g, b := huecolor.ConvertRGB(float64(hueLight.State.XY[0]), float64(hueLight.State.XY[1]), float64(hueLight.State.Bri)/255)
  212. light := models.Light{
  213. ID: -1,
  214. Name: hueLight.Name,
  215. BridgeID: bridge.ID,
  216. InternalID: hueLight.UniqueID,
  217. On: hueLight.State.On,
  218. }
  219. light.SetColorRGBf(r, g, b)
  220. light.Brightness = hueLight.State.Bri
  221. lights = append(lights, light)
  222. }
  223. return lights, nil
  224. }
  225. func (d *driver) Bridges(ctx context.Context) ([]models.Bridge, error) {
  226. hueBridges, err := gohue.FindBridges()
  227. if err != nil {
  228. if err.Error() == "no bridges found" { // It's not my fault the library doesn't have good errors. D:<
  229. return []models.Bridge{}, nil
  230. }
  231. return nil, err
  232. }
  233. bridges := make([]models.Bridge, 0, len(hueBridges))
  234. for _, hueBridge := range hueBridges {
  235. bridges = append(bridges, models.Bridge{
  236. ID: -1,
  237. InternalID: hueBridge.Info.Device.SerialNumber,
  238. Driver: "hue",
  239. Name: fmt.Sprintf("New bridge (%s, %s)", hueBridge.Info.Device.FriendlyName, hueBridge.Info.Device.SerialNumber),
  240. Addr: hueBridge.IPAddress,
  241. })
  242. }
  243. return bridges, err
  244. }
  245. func (d *driver) Connect(ctx context.Context, bridge models.Bridge) (models.Bridge, error) {
  246. hueBridge, err := gohue.NewBridge(bridge.Addr)
  247. if err != nil {
  248. return models.Bridge{}, err
  249. }
  250. // Make 30 attempts (30 seconds)
  251. attempts := 30
  252. for attempts > 0 {
  253. key, err := hueBridge.CreateUser("Lucifer (git.aiterp.net/lucifer/lucifer)")
  254. if len(key) > 0 && err == nil {
  255. bridge.Key = []byte(key)
  256. bridge.InternalID = hueBridge.Info.Device.SerialNumber
  257. return bridge, nil
  258. }
  259. select {
  260. case <-time.After(time.Second):
  261. attempts--
  262. case <-ctx.Done():
  263. return models.Bridge{}, ctx.Err()
  264. }
  265. }
  266. return models.Bridge{}, errors.New("Bridge discovery timed out after 30 failed attempts")
  267. }
  268. func (d *driver) ChangedLights(ctx context.Context, bridge models.Bridge, lights ...models.Light) ([]models.Light, error) {
  269. hueBridge, err := d.getBridge(bridge)
  270. if err != nil {
  271. return nil, err
  272. }
  273. hueLights, err := hueBridge.GetAllLights()
  274. if err != nil {
  275. return nil, err
  276. }
  277. subset := make([]models.Light, 0, len(lights))
  278. for _, hueLight := range hueLights {
  279. for _, light := range lights {
  280. if hueLight.UniqueID != light.InternalID {
  281. continue
  282. }
  283. d.mutex.Lock()
  284. c, cOk := d.colors[colorKey(light)]
  285. d.mutex.Unlock()
  286. 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 {
  287. subset = append(subset, light)
  288. }
  289. break
  290. }
  291. }
  292. return subset, nil
  293. }
  294. func (d *driver) ForgetLight(ctx context.Context, bridge models.Bridge, light models.Light) error {
  295. hueBridge, err := d.getBridge(bridge)
  296. if err != nil {
  297. return err
  298. }
  299. hueLights, err := hueBridge.GetAllLights()
  300. if err != nil {
  301. return err
  302. }
  303. for _, hueLight := range hueLights {
  304. if light.InternalID == hueLight.UniqueID {
  305. return hueLight.Delete()
  306. }
  307. }
  308. return httperr.NotFound("Light")
  309. }
  310. func (d *driver) calcColor(light models.Light, hueLight gohue.Light) (x, y float64, bri uint8, err error) {
  311. r, g, b, err := light.ColorRGBf()
  312. if err != nil {
  313. return
  314. }
  315. x, y = huecolor.ConvertXY(r, g, b)
  316. bri = light.Brightness
  317. if bri < 1 {
  318. bri = 1
  319. } else if bri > 254 {
  320. bri = 254
  321. }
  322. return
  323. }
  324. func (d *driver) getRawState(hueLight gohue.Light) (map[string]interface{}, error) {
  325. data := struct {
  326. State map[string]interface{} `json:"state"`
  327. }{
  328. State: make(map[string]interface{}, 16),
  329. }
  330. uri := fmt.Sprintf("/api/%s/lights/%d/", hueLight.Bridge.Username, hueLight.Index)
  331. _, reader, err := hueLight.Bridge.Get(uri)
  332. if err != nil {
  333. return nil, err
  334. }
  335. err = json.NewDecoder(reader).Decode(&data)
  336. return data.State, err
  337. }
  338. func (d *driver) setState(hueLight gohue.Light, key string, value interface{}) error {
  339. m := make(map[string]interface{}, 1)
  340. m[key] = value
  341. uri := fmt.Sprintf("/api/%s/lights/%d/state", hueLight.Bridge.Username, hueLight.Index)
  342. _, _, err := hueLight.Bridge.Put(uri, m)
  343. return err
  344. }
  345. func (d *driver) getBridge(bridge models.Bridge) (*gohue.Bridge, error) {
  346. d.mutex.Lock()
  347. defer d.mutex.Unlock()
  348. if hueBridge, ok := d.bridges[bridge.ID]; ok {
  349. return hueBridge, nil
  350. }
  351. hueBridge, err := gohue.NewBridge(bridge.Addr)
  352. if err != nil {
  353. return nil, err
  354. }
  355. if err := hueBridge.GetInfo(); err != nil {
  356. return nil, err
  357. }
  358. if hueBridge.Info.Device.SerialNumber != bridge.InternalID {
  359. return nil, errors.New("Serial number does not match hardware")
  360. }
  361. err = hueBridge.Login(string(bridge.Key))
  362. if err != nil {
  363. return nil, err
  364. }
  365. d.bridges[bridge.ID] = hueBridge
  366. return hueBridge, nil
  367. }
  368. func diff(a, b float32) float32 {
  369. diff := a - b
  370. if diff < 0 {
  371. return -diff
  372. }
  373. return diff
  374. }
  375. func init() {
  376. driver := &driver{
  377. bridges: make(map[int]*gohue.Bridge, 16),
  378. colors: make(map[string]xyBri, 128),
  379. }
  380. light.RegisterDriver("hue", driver)
  381. }