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.

255 lines
5.5 KiB

  1. package tradfri
  2. import (
  3. "fmt"
  4. lucifer3 "git.aiterp.net/lucifer3/server"
  5. "git.aiterp.net/lucifer3/server/device"
  6. "git.aiterp.net/lucifer3/server/events"
  7. "git.aiterp.net/lucifer3/server/internal/color"
  8. "git.aiterp.net/lucifer3/server/internal/gentools"
  9. "github.com/eriklupander/tradfri-go/tradfri"
  10. "github.com/pkg/errors"
  11. "math"
  12. "math/rand"
  13. "strings"
  14. "sync"
  15. "time"
  16. )
  17. type Bridge struct {
  18. mx sync.Mutex
  19. client *tradfri.Client
  20. id string
  21. newIP string
  22. newCredentials string
  23. internalMap map[string]int
  24. nameMap map[string]string
  25. stateMap map[string]device.State
  26. }
  27. func connect(ip, credentials string) (bridge *Bridge, retErr error) {
  28. bridge = &Bridge{}
  29. defer func() {
  30. retErr = catchPanic()
  31. }()
  32. if !strings.Contains(ip, ":") {
  33. ip = ip + ":5684"
  34. }
  35. parts := strings.Split(credentials, ":")
  36. if len(parts) == 1 {
  37. bridge.client = tradfri.NewTradfriClient(ip, "Client_identity", parts[0])
  38. clientID := fmt.Sprintf("Lucifer4_%d", rand.Intn(10000))
  39. token, err := bridge.client.AuthExchange(clientID)
  40. if err != nil {
  41. return nil, err
  42. }
  43. bridge.newCredentials = clientID + ":" + token.Token
  44. } else {
  45. bridge.client = tradfri.NewTradfriClient(ip, parts[0], parts[1])
  46. bridge.newCredentials = parts[0] + ":" + parts[1]
  47. }
  48. bridge.newIP = ip
  49. bridge.id = fmt.Sprintf("tradfri:%s", ip)
  50. return
  51. }
  52. func (b *Bridge) listen(bus *lucifer3.EventBus) {
  53. b.internalMap = make(map[string]int, 16)
  54. b.nameMap = make(map[string]string, 16)
  55. b.stateMap = make(map[string]device.State, 16)
  56. go func() {
  57. defer b.mx.Unlock()
  58. for {
  59. b.mx.Lock()
  60. devices, err := b.client.ListDevices()
  61. if err != nil {
  62. bus.RunEvent(events.DeviceFailed{
  63. ID: b.id,
  64. Error: "Unable to fetch IKEA devices: " + err.Error(),
  65. })
  66. return
  67. }
  68. for _, ikeaDevice := range devices {
  69. id := fmt.Sprintf("%s:%d", b.id, ikeaDevice.DeviceId)
  70. lc := ikeaDevice.LightControl
  71. if len(lc) == 0 {
  72. continue
  73. }
  74. currState := device.State{
  75. Power: gentools.Ptr(lc[0].Power > 0),
  76. Intensity: gentools.Ptr(float64(lc[0].Dimmer) / 254),
  77. Color: &color.Color{
  78. XY: &color.XY{
  79. X: float64(lc[0].CIE_1931_X) / 65535,
  80. Y: float64(lc[0].CIE_1931_Y) / 65535,
  81. },
  82. },
  83. }
  84. if len(b.nameMap[id]) == 0 {
  85. b.internalMap[id] = ikeaDevice.DeviceId
  86. b.nameMap[id] = ikeaDevice.Name
  87. b.stateMap[id] = currState
  88. bus.RunEvent(events.DeviceReady{ID: id})
  89. bus.RunEvent(events.HardwareMetadata{
  90. ID: id,
  91. Icon: "lightbulb",
  92. })
  93. bus.RunEvent(events.HardwareState{
  94. ID: id,
  95. InternalName: ikeaDevice.Name,
  96. SupportFlags: defaultSF,
  97. ColorFlags: defaultCF,
  98. State: b.stateMap[id],
  99. })
  100. }
  101. b.refreshDevice(id, bus)
  102. }
  103. b.mx.Unlock()
  104. time.Sleep(10 * time.Second)
  105. }
  106. }()
  107. }
  108. func (b *Bridge) writeState(id string, state device.State, bus *lucifer3.EventBus) {
  109. b.stateMap[id] = state
  110. b.refreshDevice(id, bus)
  111. }
  112. func (b *Bridge) refreshDevice(id string, bus *lucifer3.EventBus) {
  113. ikeaDevice, err := b.client.GetDevice(b.internalMap[id])
  114. if err != nil {
  115. bus.RunEvent(events.DeviceFailed{
  116. ID: id,
  117. Error: "Unable to fetch IKEA device",
  118. })
  119. return
  120. }
  121. changed := false
  122. if len(ikeaDevice.LightControl) > 0 {
  123. ikeaState := ikeaDevice.LightControl[0]
  124. luciferState := b.stateMap[id]
  125. currPower := ikeaState.Power > 0
  126. currIntensity := float64(ikeaState.Dimmer) / 254
  127. currX := float64(ikeaState.CIE_1931_X) / 65535
  128. currY := float64(ikeaState.CIE_1931_Y) / 65535
  129. if luciferState.Intensity != nil {
  130. newIntensity := *luciferState.Intensity
  131. diffIntensity := math.Abs(newIntensity - currIntensity)
  132. if diffIntensity >= 0.01 {
  133. changed = true
  134. intensityInt := int(math.Round(newIntensity * 254))
  135. _, err := b.client.PutDeviceDimming(ikeaDevice.DeviceId, intensityInt)
  136. if err != nil {
  137. bus.RunEvent(events.DeviceFailed{
  138. ID: id,
  139. Error: "Failed to update intensity state",
  140. })
  141. return
  142. }
  143. }
  144. }
  145. if luciferState.Color != nil {
  146. col, ok := luciferState.Color.ToXY()
  147. if !ok {
  148. return
  149. }
  150. newX := col.XY.X
  151. newY := col.XY.Y
  152. diffX := math.Abs(newX - currX)
  153. diffY := math.Abs(newY - currY)
  154. if diffX >= 0.0001 || diffY >= 0.0001 {
  155. changed = true
  156. xInt := int(math.Round(newX * 65535))
  157. yInt := int(math.Round(newY * 65535))
  158. _, err := b.client.PutDeviceColor(ikeaDevice.DeviceId, xInt, yInt)
  159. if err != nil {
  160. bus.RunEvent(events.DeviceFailed{
  161. ID: id,
  162. Error: "Failed to update color state",
  163. })
  164. return
  165. }
  166. }
  167. }
  168. if luciferState.Power != nil {
  169. newPower := *luciferState.Power
  170. diffPower := newPower != currPower
  171. if diffPower {
  172. changed = true
  173. powerInt := 0
  174. if newPower {
  175. powerInt = 1
  176. }
  177. _, err := b.client.PutDevicePower(ikeaDevice.DeviceId, powerInt)
  178. if err != nil {
  179. bus.RunEvent(events.DeviceFailed{
  180. ID: id,
  181. Error: "Failed to update power state",
  182. })
  183. return
  184. }
  185. }
  186. }
  187. if changed {
  188. bus.RunEvent(events.HardwareState{
  189. ID: id,
  190. InternalName: ikeaDevice.Name,
  191. SupportFlags: defaultSF,
  192. ColorFlags: defaultCF,
  193. State: luciferState,
  194. })
  195. }
  196. }
  197. }
  198. const defaultSF = device.SFlagPower | device.SFlagIntensity | device.SFlagColor
  199. const defaultCF = device.CFlagXY
  200. func catchPanic(mutexes ...sync.Locker) (retErr error) {
  201. if err, ok := recover().(error); ok {
  202. retErr = errors.Wrap(err, "panik:")
  203. }
  204. for _, mx := range mutexes {
  205. mx.Unlock()
  206. }
  207. return
  208. }