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.

441 lines
9.4 KiB

3 years ago
3 years ago
  1. package nanoleaf
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "fmt"
  7. "git.aiterp.net/lucifer/new-server/models"
  8. "github.com/lucasb-eyer/go-colorful"
  9. "io"
  10. "io/ioutil"
  11. "log"
  12. "math"
  13. "net"
  14. "net/http"
  15. "strconv"
  16. "strings"
  17. "sync"
  18. "time"
  19. )
  20. type bridge struct {
  21. mu sync.Mutex
  22. externalID int
  23. host string
  24. apiKey string
  25. panels []*panel
  26. panelIDMap map[uint16]int
  27. }
  28. func (b *bridge) Devices() []models.Device {
  29. results := make([]models.Device, 0, len(b.panels))
  30. for i, panel := range b.panels {
  31. red := float64(panel.ColorRGBA[0]) / 255.0
  32. green := float64(panel.ColorRGBA[1]) / 255.0
  33. blue := float64(panel.ColorRGBA[2]) / 255.0
  34. hue, sat, value := colorful.LinearRgb(red, green, blue).Hsv()
  35. shapeType, shapeTypeOK := shapeTypeMap[panel.ShapeType]
  36. if !shapeTypeOK {
  37. shapeType = "NanoLeaf"
  38. }
  39. shapeIcon, shapeIconOK := shapeIconMap[panel.ShapeType]
  40. if !shapeIconOK {
  41. shapeIcon = "lightbulb"
  42. }
  43. results = append(results, models.Device{
  44. ID: -1,
  45. BridgeID: b.externalID,
  46. InternalID: strconv.Itoa(int(panel.ID)),
  47. Icon: shapeIcon,
  48. Name: fmt.Sprintf("%s %d", shapeType, i),
  49. Capabilities: []models.DeviceCapability{
  50. models.DCPower,
  51. models.DCColorHS,
  52. models.DCIntensity,
  53. models.DCButtons,
  54. },
  55. ButtonNames: []string{"Touch"},
  56. DriverProperties: map[string]interface{}{
  57. "x": panel.X,
  58. "y": panel.Y,
  59. "o": panel.O,
  60. "shapeType": shapeTypeMap[panel.ShapeType],
  61. "shapeWidth": shapeWidthMap[panel.ShapeType],
  62. },
  63. UserProperties: nil,
  64. State: models.DeviceState{
  65. Power: panel.ColorRGBA[3] == 0,
  66. Color: models.ColorValue{
  67. Hue: math.Mod(hue, 360),
  68. Saturation: sat,
  69. },
  70. Intensity: value,
  71. Temperature: 0,
  72. },
  73. Tags: nil,
  74. })
  75. }
  76. return results
  77. }
  78. func (b *bridge) Refresh(ctx context.Context) error {
  79. overview, err := b.Overview(ctx)
  80. if err != nil {
  81. return err
  82. }
  83. b.mu.Lock()
  84. PanelLoop:
  85. for _, panelInfo := range overview.PanelLayout.Data.PositionData {
  86. if shapeTypeMap[panelInfo.ShapeType] == "Shapes Controller" {
  87. continue
  88. }
  89. for _, existingPanel := range b.panels {
  90. if existingPanel.ID == panelInfo.PanelID {
  91. existingPanel.O = panelInfo.O
  92. existingPanel.X = panelInfo.X
  93. existingPanel.Y = panelInfo.Y
  94. existingPanel.ShapeType = panelInfo.ShapeType
  95. continue PanelLoop
  96. }
  97. }
  98. b.panels = append(b.panels, &panel{
  99. ID: panelInfo.PanelID,
  100. ColorRGBA: [4]byte{0, 0, 0, 0},
  101. TransitionAt: time.Time{},
  102. O: panelInfo.O,
  103. X: panelInfo.X,
  104. Y: panelInfo.Y,
  105. ShapeType: panelInfo.ShapeType,
  106. Stale: true,
  107. SlowUpdates: 5,
  108. })
  109. }
  110. b.mu.Unlock()
  111. return nil
  112. }
  113. func (b *bridge) Overview(ctx context.Context) (*Overview, error) {
  114. req, err := http.NewRequest("GET", b.URL(), nil)
  115. if err != nil {
  116. return nil, err
  117. }
  118. res, err := http.DefaultClient.Do(req.WithContext(ctx))
  119. if err != nil {
  120. return nil, err
  121. }
  122. defer res.Body.Close()
  123. switch res.StatusCode {
  124. case 400, 403, 500, 503:
  125. return nil, models.ErrUnexpectedResponse
  126. case 401:
  127. return nil, models.ErrIncorrectToken
  128. }
  129. overview := Overview{}
  130. err = json.NewDecoder(res.Body).Decode(&overview)
  131. if err != nil {
  132. return nil, err
  133. }
  134. return &overview, nil
  135. }
  136. func (b *bridge) URL(resource ...string) string {
  137. return fmt.Sprintf("http://%s:16021/api/v1/%s/%s", b.host, b.apiKey, strings.Join(resource, "/"))
  138. }
  139. func (b *bridge) Update(devices []models.Device) {
  140. b.mu.Lock()
  141. defer b.mu.Unlock()
  142. for _, device := range devices {
  143. id, err := strconv.Atoi(device.InternalID)
  144. if err != nil {
  145. continue
  146. }
  147. b.panelIDMap[uint16(id)] = device.ID
  148. for _, panel := range b.panels {
  149. if panel.ID == uint16(id) {
  150. if device.State.Power {
  151. color := colorful.Hsv(device.State.Color.Hue, device.State.Color.Saturation, device.State.Intensity)
  152. red, green, blue := color.RGB255()
  153. newColor := [4]byte{red, green, blue, 255}
  154. if newColor != panel.ColorRGBA {
  155. panel.update(newColor, time.Now().Add(time.Millisecond*120))
  156. }
  157. } else {
  158. panel.update([4]byte{0, 0, 0, 0}, time.Now())
  159. }
  160. break
  161. }
  162. }
  163. }
  164. }
  165. func (b *bridge) Run(ctx context.Context, info models.Bridge, ch chan<- models.Event) error {
  166. err := b.updateEffect(ctx)
  167. if err != nil {
  168. return err
  169. }
  170. conn, err := net.DialUDP("udp", nil, &net.UDPAddr{
  171. IP: net.ParseIP(info.Address),
  172. Port: 60222,
  173. })
  174. if err != nil {
  175. return err
  176. }
  177. defer conn.Close()
  178. // Notify connections and disconnections
  179. ch <- models.BridgeConnectedEvent(info)
  180. defer func() {
  181. ch <- models.BridgeDisconnectedEvent(info)
  182. }()
  183. // Start touch listener. This one should go down together with this one, though, so it needs a new context.
  184. ctx2, cancel := context.WithCancel(ctx)
  185. defer cancel()
  186. go b.runTouchListener(ctx2, info.Address, info.Token, ch)
  187. go func() {
  188. ticker := time.NewTicker(time.Second * 5)
  189. hadErrors := false
  190. for {
  191. select {
  192. case <-ticker.C:
  193. case <-ctx2.Done():
  194. }
  195. reqTimeout, cancel := context.WithTimeout(ctx2, time.Second*4)
  196. err := b.updateEffect(reqTimeout)
  197. cancel()
  198. if err != nil {
  199. log.Println("Failed to update effects:", err, "This error is non-fatal, and it will be retried shortly.")
  200. hadErrors = true
  201. } else if hadErrors {
  202. b.mu.Lock()
  203. for _, panel := range b.panels {
  204. panel.Stale = true
  205. panel.SlowUpdates = 3
  206. panel.TicksUntilSlowUpdate = 10
  207. }
  208. b.mu.Unlock()
  209. hadErrors = false
  210. }
  211. }
  212. }()
  213. ticker := time.NewTicker(time.Millisecond * 100)
  214. defer ticker.Stop()
  215. strikes := 0
  216. for range ticker.C {
  217. if ctx.Err() != nil {
  218. break
  219. }
  220. panelUpdate := make(PanelUpdate, 2)
  221. b.mu.Lock()
  222. for _, panel := range b.panels {
  223. if !panel.Stale {
  224. if panel.SlowUpdates > 0 {
  225. panel.TicksUntilSlowUpdate -= 1
  226. if panel.TicksUntilSlowUpdate > 0 {
  227. continue
  228. }
  229. panel.TicksUntilSlowUpdate = 10
  230. panel.SlowUpdates -= 1
  231. } else {
  232. continue
  233. }
  234. } else {
  235. panel.Stale = false
  236. }
  237. panelUpdate.Add(panel.message())
  238. if panelUpdate.Len() > 150 {
  239. break
  240. }
  241. }
  242. b.mu.Unlock()
  243. if panelUpdate.Len() == 0 {
  244. continue
  245. }
  246. _, err := conn.Write(panelUpdate)
  247. if err != nil {
  248. strikes++
  249. if strikes >= 3 {
  250. return err
  251. }
  252. } else {
  253. strikes = 0
  254. }
  255. }
  256. return nil
  257. }
  258. func (b *bridge) updateEffect(ctx context.Context) error {
  259. overview, err := b.Overview(ctx)
  260. if err != nil {
  261. return err
  262. }
  263. if overview.Effects.Select == "*Dynamic*" && overview.State.ColorMode == "effect" {
  264. return nil
  265. }
  266. req, err := http.NewRequest("PUT", b.URL("effects"), bytes.NewReader(httpMessage))
  267. if err != nil {
  268. return err
  269. }
  270. res, err := http.DefaultClient.Do(req.WithContext(ctx))
  271. if err != nil {
  272. return err
  273. }
  274. defer res.Body.Close()
  275. if res.StatusCode != 204 {
  276. return models.ErrUnexpectedResponse
  277. }
  278. b.mu.Lock()
  279. for _, panel := range b.panels {
  280. panel.Stale = true
  281. }
  282. b.mu.Unlock()
  283. return nil
  284. }
  285. func (b *bridge) runTouchListener(ctx context.Context, host, apiKey string, ch chan<- models.Event) {
  286. cooldownID := 0
  287. cooldownUntil := time.Now()
  288. message := make(PanelEventMessage, 65536)
  289. reqCloser := io.Closer(nil)
  290. for {
  291. // Set up touch event receiver
  292. touchListener, err := net.ListenUDP("udp4", &net.UDPAddr{
  293. Port: 0,
  294. IP: net.IPv4(0, 0, 0, 0),
  295. })
  296. if err != nil {
  297. log.Println("Socket error:", err)
  298. goto teardownAndRetry
  299. }
  300. {
  301. // Create touch event sender on the remote end.
  302. req, err := http.NewRequest("GET", fmt.Sprintf("http://%s:16021/api/v1/%s/events?id=4", host, apiKey), nil)
  303. if err != nil {
  304. log.Println("HTTP error:", err)
  305. goto teardownAndRetry
  306. }
  307. req.Header["TouchEventsPort"] = []string{strconv.Itoa(touchListener.LocalAddr().(*net.UDPAddr).Port)}
  308. res, err := http.DefaultClient.Do(req.WithContext(ctx))
  309. if err != nil {
  310. log.Println("HTTP error:", err)
  311. goto teardownAndRetry
  312. }
  313. // Discard all data coming over http.
  314. reqCloser = res.Body
  315. go io.Copy(ioutil.Discard, res.Body)
  316. for {
  317. if ctx.Err() != nil {
  318. goto teardownAndRetry
  319. }
  320. // Check in with the context every so often
  321. _ = touchListener.SetReadDeadline(time.Now().Add(time.Second))
  322. n, _, err := touchListener.ReadFromUDP(message)
  323. if err != nil {
  324. if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
  325. continue
  326. } else if ctx.Err() == nil {
  327. log.Println("UDP error:", err)
  328. }
  329. goto teardownAndRetry
  330. }
  331. if !message[:n].ValidateLength() {
  332. log.Println("Bad message length field")
  333. continue
  334. }
  335. for i := 0; i < message.Count(); i += 1 {
  336. b.mu.Lock()
  337. externalID, hasExternalID := b.panelIDMap[message.PanelID(i)]
  338. swipedFromID, hasSwipedFromID := b.panelIDMap[message.SwipedFromPanelID(i)]
  339. b.mu.Unlock()
  340. if !hasExternalID || (externalID == cooldownID && time.Now().Before(cooldownUntil)) {
  341. continue
  342. }
  343. event := models.Event{
  344. Name: models.ENButtonPressed,
  345. Payload: map[string]string{
  346. "buttonIndex": "0",
  347. "buttonName": "Touch",
  348. "deviceId": strconv.Itoa(externalID),
  349. },
  350. }
  351. if hasSwipedFromID {
  352. event.Payload["swipedFromDeviceId"] = strconv.Itoa(swipedFromID)
  353. }
  354. ch <- event
  355. cooldownID = externalID
  356. cooldownUntil = time.Now().Add(time.Second)
  357. }
  358. }
  359. }
  360. teardownAndRetry:
  361. if touchListener != nil {
  362. _ = touchListener.Close()
  363. }
  364. if reqCloser != nil {
  365. _ = reqCloser.Close()
  366. reqCloser = nil
  367. }
  368. if ctx.Err() != nil {
  369. break
  370. }
  371. time.Sleep(time.Second * 3)
  372. }
  373. }