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.

334 lines
7.7 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/internal/gentools"
  9. "git.aiterp.net/lucifer3/server/services/script"
  10. "git.aiterp.net/lucifer3/server/services/uistate"
  11. "github.com/google/uuid"
  12. "github.com/gorilla/websocket"
  13. "github.com/labstack/echo/v4"
  14. "github.com/labstack/echo/v4/middleware"
  15. "log"
  16. "net"
  17. "net/http"
  18. "sync"
  19. "time"
  20. )
  21. var zeroUUID = uuid.UUID{
  22. 0, 0, 0, 0,
  23. 0, 0, 0, 0,
  24. 0, 0, 0, 0,
  25. 0, 0, 0, 0,
  26. }
  27. func New(addr string) (lucifer3.Service, error) {
  28. svc := &service{}
  29. upgrader := websocket.Upgrader{
  30. CheckOrigin: func(r *http.Request) bool {
  31. return true
  32. },
  33. }
  34. e := echo.New()
  35. e.HideBanner = true
  36. e.HidePort = true
  37. e.Use(middleware.CORS())
  38. e.GET("/color/:value", func(c echo.Context) error {
  39. col, err := color.Parse(c.Param("value"))
  40. if err != nil {
  41. return c.String(400, err.Error())
  42. }
  43. rgb, _ := col.ToRGB()
  44. xy, _ := col.ToXY()
  45. hs, _ := col.ToHS()
  46. return c.JSON(200, color.Color{
  47. K: col.K,
  48. RGB: rgb.RGB,
  49. HS: hs.HS,
  50. XY: xy.XY,
  51. })
  52. })
  53. e.GET("/state", func(c echo.Context) error {
  54. svc.mu.Lock()
  55. data := svc.data
  56. svc.mu.Unlock()
  57. return c.JSON(200, data)
  58. })
  59. e.GET("/subscribe", func(c echo.Context) error {
  60. ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
  61. if err != nil {
  62. return err
  63. }
  64. sub := make(chan uistate.Patch, 64)
  65. svc.addSub(sub)
  66. defer svc.removeSub(sub)
  67. defer ws.Close()
  68. lastSent := time.Now()
  69. patches := make([]uistate.Patch, 0, 128)
  70. for {
  71. patch := <-sub
  72. since := time.Now().Sub(lastSent)
  73. patches = append(patches[:0], patch)
  74. if since < time.Millisecond*950 {
  75. waitCh := time.NewTimer(time.Millisecond*1000 - since)
  76. waiting := true
  77. for waiting {
  78. select {
  79. case patch, ok := <-sub:
  80. patches = append(patches, patch)
  81. waiting = ok
  82. case <-waitCh.C:
  83. waiting = false
  84. }
  85. }
  86. }
  87. err := ws.WriteJSON(patches)
  88. if err != nil {
  89. break
  90. }
  91. lastSent = time.Now()
  92. }
  93. return nil
  94. })
  95. e.GET("/subscribe-simple", func(c echo.Context) error {
  96. type ChangedPatch struct {
  97. Devices map[string]*uistate.Device `json:"devices"`
  98. Assignments map[uuid.UUID]*uistate.Assignment `json:"assignments"`
  99. Scripts map[string][]script.Line `json:"scripts"`
  100. Full *uistate.Data `json:"full,omitempty"`
  101. }
  102. ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
  103. if err != nil {
  104. return err
  105. }
  106. sub := make(chan uistate.Patch, 64)
  107. svc.addSub(sub)
  108. defer svc.removeSub(sub)
  109. defer ws.Close()
  110. svc.mu.Lock()
  111. err = ws.WriteJSON(ChangedPatch{Full: &svc.data})
  112. svc.mu.Unlock()
  113. if err != nil {
  114. return err
  115. }
  116. patches := make([]uistate.Patch, 0, 128)
  117. for {
  118. patch := <-sub
  119. patches = append(patches[:0], patch)
  120. waitCh := time.After(time.Millisecond * 330)
  121. waiting := true
  122. for waiting {
  123. select {
  124. case patch, ok := <-sub:
  125. patches = append(patches, patch)
  126. waiting = ok
  127. case <-waitCh:
  128. waiting = false
  129. }
  130. }
  131. statePatch := ChangedPatch{
  132. Devices: make(map[string]*uistate.Device),
  133. Assignments: make(map[uuid.UUID]*uistate.Assignment),
  134. Scripts: make(map[string][]script.Line),
  135. Full: nil,
  136. }
  137. svc.mu.Lock()
  138. for _, patch := range patches {
  139. if patch.Device != nil {
  140. if patch.Device.Delete {
  141. statePatch.Devices[patch.Device.ID] = nil
  142. } else {
  143. statePatch.Devices[patch.Device.ID] = gentools.Ptr(svc.data.Devices[patch.Device.ID])
  144. }
  145. }
  146. if patch.Assignment != nil {
  147. if patch.Assignment.Delete {
  148. statePatch.Assignments[patch.Assignment.ID] = nil
  149. } else {
  150. statePatch.Assignments[patch.Assignment.ID] = gentools.Ptr(svc.data.Assignments[patch.Assignment.ID])
  151. }
  152. }
  153. if patch.Script != nil {
  154. statePatch.Scripts[patch.Script.Name] = svc.data.Scripts[patch.Script.Name]
  155. }
  156. }
  157. svc.mu.Unlock()
  158. err := ws.WriteJSON(statePatch)
  159. if err != nil {
  160. break
  161. }
  162. }
  163. return nil
  164. })
  165. e.POST("/command", func(c echo.Context) error {
  166. var input commandInput
  167. err := c.Bind(&input)
  168. if err != nil {
  169. return err
  170. }
  171. svc.mu.Lock()
  172. bus := svc.bus
  173. svc.mu.Unlock()
  174. if bus == nil {
  175. return c.String(413, "Waiting for bus")
  176. }
  177. switch {
  178. case input.Assign != nil:
  179. bus.RunCommand(commands.Assign{
  180. ID: nil,
  181. Match: input.Assign.Match,
  182. Effect: input.Assign.Effect.Effect,
  183. })
  184. case input.PairDevice != nil:
  185. bus.RunCommand(*input.PairDevice)
  186. case input.ConnectDevice != nil:
  187. bus.RunCommand(*input.ConnectDevice)
  188. case input.ForgetDevice != nil:
  189. bus.RunCommand(*input.ForgetDevice)
  190. case input.SearchDevices != nil:
  191. bus.RunCommand(*input.SearchDevices)
  192. case input.AddAlias != nil:
  193. bus.RunCommand(*input.AddAlias)
  194. case input.RemoveAlias != nil:
  195. bus.RunCommand(*input.RemoveAlias)
  196. case input.UpdateScript != nil:
  197. bus.RunCommand(*input.UpdateScript)
  198. case input.ExecuteScript != nil:
  199. bus.RunCommand(*input.ExecuteScript)
  200. case input.UpdateTrigger != nil:
  201. if input.UpdateTrigger.ID == zeroUUID {
  202. input.UpdateTrigger.ID = uuid.New()
  203. }
  204. if input.UpdateTrigger.ScriptPre == nil {
  205. input.UpdateTrigger.ScriptPre = make([]script.Line, 0)
  206. }
  207. if input.UpdateTrigger.ScriptPost == nil {
  208. input.UpdateTrigger.ScriptPost = make([]script.Line, 0)
  209. }
  210. bus.RunCommand(*input.UpdateTrigger)
  211. case input.DeleteTrigger != nil:
  212. bus.RunCommand(*input.DeleteTrigger)
  213. default:
  214. return c.String(400, "No supported command found in input")
  215. }
  216. return c.JSON(200, input)
  217. })
  218. listener, err := net.Listen("tcp", addr)
  219. if err != nil {
  220. return nil, err
  221. }
  222. e.Listener = listener
  223. go func() {
  224. err := e.Start(addr)
  225. if err != nil {
  226. log.Fatalln("Failed to listen to webserver")
  227. }
  228. }()
  229. return svc, nil
  230. }
  231. type service struct {
  232. mu sync.Mutex
  233. data uistate.Data
  234. bus *lucifer3.EventBus
  235. subs []chan uistate.Patch
  236. }
  237. func (s *service) Active() bool {
  238. return true
  239. }
  240. func (s *service) HandleEvent(bus *lucifer3.EventBus, event lucifer3.Event) {
  241. switch event := event.(type) {
  242. case events.Started:
  243. s.mu.Lock()
  244. s.bus = bus
  245. s.mu.Unlock()
  246. case uistate.Patch:
  247. s.mu.Lock()
  248. s.data = s.data.WithPatch(event)
  249. subs := s.subs
  250. s.mu.Unlock()
  251. for _, sub := range subs {
  252. select {
  253. case sub <- event:
  254. default:
  255. }
  256. }
  257. }
  258. }
  259. func (s *service) addSub(ch chan uistate.Patch) {
  260. s.mu.Lock()
  261. s.subs = append(s.subs, ch)
  262. s.mu.Unlock()
  263. }
  264. func (s *service) removeSub(ch chan uistate.Patch) {
  265. s.mu.Lock()
  266. s.subs = append([]chan uistate.Patch{}, s.subs...)
  267. for i, sub := range s.subs {
  268. if sub == ch {
  269. s.subs = append(s.subs[:i], s.subs[i+1:]...)
  270. break
  271. }
  272. }
  273. s.mu.Unlock()
  274. }
  275. type commandInput struct {
  276. Assign *assignInput `json:"assign,omitempty"`
  277. AddAlias *commands.AddAlias `json:"addAlias,omitempty"`
  278. RemoveAlias *commands.RemoveAlias `json:"removeAlias,omitempty"`
  279. PairDevice *commands.PairDevice `json:"pairDevice,omitempty"`
  280. ConnectDevice *commands.ConnectDevice `json:"connectDevice,omitempty"`
  281. SearchDevices *commands.SearchDevices `json:"searchDevices,omitempty"`
  282. ForgetDevice *commands.ForgetDevice `json:"forgetDevice,omitempty"`
  283. UpdateScript *script.Update `json:"updateScript,omitempty"`
  284. ExecuteScript *script.Execute `json:"executeScript,omitempty"`
  285. UpdateTrigger *script.UpdateTrigger `json:"updateTrigger,omitempty"`
  286. DeleteTrigger *script.DeleteTrigger `json:"deleteTrigger,omitempty"`
  287. }
  288. type assignInput struct {
  289. Match string `json:"match"`
  290. Effect effects.Serializable `json:"effect"`
  291. }