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.

338 lines
7.8 KiB

1 year ago
2 years ago
2 years ago
1 year 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. if len(patch.Script.Lines) > 0 {
  155. statePatch.Scripts[patch.Script.Name] = svc.data.Scripts[patch.Script.Name]
  156. } else {
  157. statePatch.Scripts[patch.Script.Name] = nil
  158. }
  159. }
  160. }
  161. svc.mu.Unlock()
  162. err := ws.WriteJSON(statePatch)
  163. if err != nil {
  164. break
  165. }
  166. }
  167. return nil
  168. })
  169. e.POST("/command", func(c echo.Context) error {
  170. var input commandInput
  171. err := c.Bind(&input)
  172. if err != nil {
  173. return err
  174. }
  175. svc.mu.Lock()
  176. bus := svc.bus
  177. svc.mu.Unlock()
  178. if bus == nil {
  179. return c.String(413, "Waiting for bus")
  180. }
  181. switch {
  182. case input.Assign != nil:
  183. bus.RunCommand(commands.Assign{
  184. ID: nil,
  185. Match: input.Assign.Match,
  186. Effect: input.Assign.Effect.Effect,
  187. })
  188. case input.PairDevice != nil:
  189. bus.RunCommand(*input.PairDevice)
  190. case input.ConnectDevice != nil:
  191. bus.RunCommand(*input.ConnectDevice)
  192. case input.ForgetDevice != nil:
  193. bus.RunCommand(*input.ForgetDevice)
  194. case input.SearchDevices != nil:
  195. bus.RunCommand(*input.SearchDevices)
  196. case input.AddAlias != nil:
  197. bus.RunCommand(*input.AddAlias)
  198. case input.RemoveAlias != nil:
  199. bus.RunCommand(*input.RemoveAlias)
  200. case input.UpdateScript != nil:
  201. bus.RunCommand(*input.UpdateScript)
  202. case input.ExecuteScript != nil:
  203. bus.RunCommand(*input.ExecuteScript)
  204. case input.UpdateTrigger != nil:
  205. if input.UpdateTrigger.ID == zeroUUID {
  206. input.UpdateTrigger.ID = uuid.New()
  207. }
  208. if input.UpdateTrigger.ScriptPre == nil {
  209. input.UpdateTrigger.ScriptPre = make([]script.Line, 0)
  210. }
  211. if input.UpdateTrigger.ScriptPost == nil {
  212. input.UpdateTrigger.ScriptPost = make([]script.Line, 0)
  213. }
  214. bus.RunCommand(*input.UpdateTrigger)
  215. case input.DeleteTrigger != nil:
  216. bus.RunCommand(*input.DeleteTrigger)
  217. default:
  218. return c.String(400, "No supported command found in input")
  219. }
  220. return c.JSON(200, input)
  221. })
  222. listener, err := net.Listen("tcp", addr)
  223. if err != nil {
  224. return nil, err
  225. }
  226. e.Listener = listener
  227. go func() {
  228. err := e.Start(addr)
  229. if err != nil {
  230. log.Fatalln("Failed to listen to webserver")
  231. }
  232. }()
  233. return svc, nil
  234. }
  235. type service struct {
  236. mu sync.Mutex
  237. data uistate.Data
  238. bus *lucifer3.EventBus
  239. subs []chan uistate.Patch
  240. }
  241. func (s *service) Active() bool {
  242. return true
  243. }
  244. func (s *service) HandleEvent(bus *lucifer3.EventBus, event lucifer3.Event) {
  245. switch event := event.(type) {
  246. case events.Started:
  247. s.mu.Lock()
  248. s.bus = bus
  249. s.mu.Unlock()
  250. case uistate.Patch:
  251. s.mu.Lock()
  252. s.data = s.data.WithPatch(event)
  253. subs := s.subs
  254. s.mu.Unlock()
  255. for _, sub := range subs {
  256. select {
  257. case sub <- event:
  258. default:
  259. }
  260. }
  261. }
  262. }
  263. func (s *service) addSub(ch chan uistate.Patch) {
  264. s.mu.Lock()
  265. s.subs = append(s.subs, ch)
  266. s.mu.Unlock()
  267. }
  268. func (s *service) removeSub(ch chan uistate.Patch) {
  269. s.mu.Lock()
  270. s.subs = append([]chan uistate.Patch{}, s.subs...)
  271. for i, sub := range s.subs {
  272. if sub == ch {
  273. s.subs = append(s.subs[:i], s.subs[i+1:]...)
  274. break
  275. }
  276. }
  277. s.mu.Unlock()
  278. }
  279. type commandInput struct {
  280. Assign *assignInput `json:"assign,omitempty"`
  281. AddAlias *commands.AddAlias `json:"addAlias,omitempty"`
  282. RemoveAlias *commands.RemoveAlias `json:"removeAlias,omitempty"`
  283. PairDevice *commands.PairDevice `json:"pairDevice,omitempty"`
  284. ConnectDevice *commands.ConnectDevice `json:"connectDevice,omitempty"`
  285. SearchDevices *commands.SearchDevices `json:"searchDevices,omitempty"`
  286. ForgetDevice *commands.ForgetDevice `json:"forgetDevice,omitempty"`
  287. UpdateScript *script.Update `json:"updateScript,omitempty"`
  288. ExecuteScript *script.Execute `json:"executeScript,omitempty"`
  289. UpdateTrigger *script.UpdateTrigger `json:"updateTrigger,omitempty"`
  290. DeleteTrigger *script.DeleteTrigger `json:"deleteTrigger,omitempty"`
  291. }
  292. type assignInput struct {
  293. Match string `json:"match"`
  294. Effect effects.Serializable `json:"effect"`
  295. }