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.

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