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.

253 lines
5.6 KiB

1 year ago
2 years ago
2 years ago
1 year ago
  1. package httpapiv1
  2. import (
  3. lucifer3 "git.aiterp.net/lucifer3/server"
  4. "git.aiterp.net/lucifer3/server/commands"
  5. "git.aiterp.net/lucifer3/server/effects"
  6. "git.aiterp.net/lucifer3/server/events"
  7. "git.aiterp.net/lucifer3/server/internal/color"
  8. "git.aiterp.net/lucifer3/server/services/script"
  9. "git.aiterp.net/lucifer3/server/services/uistate"
  10. "github.com/google/uuid"
  11. "github.com/gorilla/websocket"
  12. "github.com/labstack/echo/v4"
  13. "github.com/labstack/echo/v4/middleware"
  14. "log"
  15. "net"
  16. "net/http"
  17. "sync"
  18. "time"
  19. )
  20. var zeroUUID = uuid.UUID{
  21. 0, 0, 0, 0,
  22. 0, 0, 0, 0,
  23. 0, 0, 0, 0,
  24. 0, 0, 0, 0,
  25. }
  26. func New(addr string) (lucifer3.Service, error) {
  27. svc := &service{}
  28. upgrader := websocket.Upgrader{
  29. CheckOrigin: func(r *http.Request) bool {
  30. return true
  31. },
  32. }
  33. e := echo.New()
  34. e.HideBanner = true
  35. e.HidePort = true
  36. e.Use(middleware.CORS())
  37. e.GET("/color/:value", func(c echo.Context) error {
  38. col, err := color.Parse(c.Param("value"))
  39. if err != nil {
  40. return c.String(400, err.Error())
  41. }
  42. rgb, _ := col.ToRGB()
  43. xy, _ := col.ToXY()
  44. hs, _ := col.ToHS()
  45. return c.JSON(200, color.Color{
  46. K: col.K,
  47. RGB: rgb.RGB,
  48. HS: hs.HS,
  49. XY: xy.XY,
  50. })
  51. })
  52. e.GET("/state", func(c echo.Context) error {
  53. svc.mu.Lock()
  54. data := svc.data
  55. svc.mu.Unlock()
  56. return c.JSON(200, data)
  57. })
  58. e.GET("/subscribe", func(c echo.Context) error {
  59. ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
  60. if err != nil {
  61. return err
  62. }
  63. sub := make(chan uistate.Patch, 64)
  64. svc.addSub(sub)
  65. defer svc.removeSub(sub)
  66. defer ws.Close()
  67. lastSent := time.Now()
  68. patches := make([]uistate.Patch, 0, 128)
  69. for {
  70. patch := <-sub
  71. since := time.Now().Sub(lastSent)
  72. patches = append(patches[:0], patch)
  73. if since < time.Millisecond*950 {
  74. waitCh := time.NewTimer(time.Millisecond*1000 - since)
  75. waiting := true
  76. for waiting {
  77. select {
  78. case patch, ok := <-sub:
  79. patches = append(patches, patch)
  80. waiting = ok
  81. case <-waitCh.C:
  82. waiting = false
  83. }
  84. }
  85. }
  86. err := ws.WriteJSON(patches)
  87. if err != nil {
  88. break
  89. }
  90. lastSent = time.Now()
  91. }
  92. return nil
  93. })
  94. e.POST("/command", func(c echo.Context) error {
  95. var input commandInput
  96. err := c.Bind(&input)
  97. if err != nil {
  98. return err
  99. }
  100. svc.mu.Lock()
  101. bus := svc.bus
  102. svc.mu.Unlock()
  103. if bus == nil {
  104. return c.String(413, "Waiting for bus")
  105. }
  106. switch {
  107. case input.Assign != nil:
  108. bus.RunCommand(commands.Assign{
  109. ID: nil,
  110. Match: input.Assign.Match,
  111. Effect: input.Assign.Effect.Effect,
  112. })
  113. case input.PairDevice != nil:
  114. bus.RunCommand(*input.PairDevice)
  115. case input.ConnectDevice != nil:
  116. bus.RunCommand(*input.ConnectDevice)
  117. case input.ForgetDevice != nil:
  118. bus.RunCommand(*input.ForgetDevice)
  119. case input.SearchDevices != nil:
  120. bus.RunCommand(*input.SearchDevices)
  121. case input.AddAlias != nil:
  122. bus.RunCommand(*input.AddAlias)
  123. case input.RemoveAlias != nil:
  124. bus.RunCommand(*input.RemoveAlias)
  125. case input.UpdateScript != nil:
  126. bus.RunCommand(*input.UpdateScript)
  127. case input.ExecuteScript != nil:
  128. bus.RunCommand(*input.ExecuteScript)
  129. case input.UpdateTrigger != nil:
  130. if input.UpdateTrigger.ID == zeroUUID {
  131. input.UpdateTrigger.ID = uuid.New()
  132. }
  133. if input.UpdateTrigger.ScriptPre == nil {
  134. input.UpdateTrigger.ScriptPre = make([]script.Line, 0)
  135. }
  136. if input.UpdateTrigger.ScriptPost == nil {
  137. input.UpdateTrigger.ScriptPost = make([]script.Line, 0)
  138. }
  139. bus.RunCommand(*input.UpdateTrigger)
  140. case input.DeleteTrigger != nil:
  141. bus.RunCommand(*input.DeleteTrigger)
  142. default:
  143. return c.String(400, "No supported command found in input")
  144. }
  145. return c.JSON(200, input)
  146. })
  147. listener, err := net.Listen("tcp", addr)
  148. if err != nil {
  149. return nil, err
  150. }
  151. e.Listener = listener
  152. go func() {
  153. err := e.Start(addr)
  154. if err != nil {
  155. log.Fatalln("Failed to listen to webserver")
  156. }
  157. }()
  158. return svc, nil
  159. }
  160. type service struct {
  161. mu sync.Mutex
  162. data uistate.Data
  163. bus *lucifer3.EventBus
  164. subs []chan uistate.Patch
  165. }
  166. func (s *service) Active() bool {
  167. return true
  168. }
  169. func (s *service) HandleEvent(bus *lucifer3.EventBus, event lucifer3.Event) {
  170. switch event := event.(type) {
  171. case events.Started:
  172. s.mu.Lock()
  173. s.bus = bus
  174. s.mu.Unlock()
  175. case uistate.Patch:
  176. s.mu.Lock()
  177. s.data = s.data.WithPatch(event)
  178. subs := s.subs
  179. s.mu.Unlock()
  180. for _, sub := range subs {
  181. select {
  182. case sub <- event:
  183. default:
  184. }
  185. }
  186. }
  187. }
  188. func (s *service) addSub(ch chan uistate.Patch) {
  189. s.mu.Lock()
  190. s.subs = append(s.subs, ch)
  191. s.mu.Unlock()
  192. }
  193. func (s *service) removeSub(ch chan uistate.Patch) {
  194. s.mu.Lock()
  195. s.subs = append([]chan uistate.Patch{}, s.subs...)
  196. for i, sub := range s.subs {
  197. if sub == ch {
  198. s.subs = append(s.subs[:i], s.subs[i+1:]...)
  199. break
  200. }
  201. }
  202. s.mu.Unlock()
  203. }
  204. type commandInput struct {
  205. Assign *assignInput `json:"assign,omitempty"`
  206. AddAlias *commands.AddAlias `json:"addAlias,omitempty"`
  207. RemoveAlias *commands.RemoveAlias `json:"removeAlias,omitempty"`
  208. PairDevice *commands.PairDevice `json:"pairDevice,omitempty"`
  209. ConnectDevice *commands.ConnectDevice `json:"connectDevice,omitempty"`
  210. SearchDevices *commands.SearchDevices `json:"searchDevices,omitempty"`
  211. ForgetDevice *commands.ForgetDevice `json:"forgetDevice,omitempty"`
  212. UpdateScript *script.Update `json:"updateScript,omitempty"`
  213. ExecuteScript *script.Execute `json:"executeScript,omitempty"`
  214. UpdateTrigger *script.UpdateTrigger `json:"updateTrigger,omitempty"`
  215. DeleteTrigger *script.DeleteTrigger `json:"deleteTrigger,omitempty"`
  216. }
  217. type assignInput struct {
  218. Match string `json:"match"`
  219. Effect effects.Serializable `json:"effect"`
  220. }