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.

361 lines
8.3 KiB

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