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

package httpapiv1
import (
lucifer3 "git.aiterp.net/lucifer3/server"
"git.aiterp.net/lucifer3/server/commands"
"git.aiterp.net/lucifer3/server/effects"
"git.aiterp.net/lucifer3/server/events"
"git.aiterp.net/lucifer3/server/internal/color"
"git.aiterp.net/lucifer3/server/internal/gentools"
"git.aiterp.net/lucifer3/server/services/script"
"git.aiterp.net/lucifer3/server/services/uistate"
"github.com/google/uuid"
"github.com/gorilla/websocket"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"io/fs"
"log"
"net"
"net/http"
"sync"
"time"
)
var zeroUUID = uuid.UUID{
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
}
func New(addr string) (lucifer3.Service, error) {
svc := &service{}
upgrader := websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
e := echo.New()
e.HideBanner = true
e.HidePort = true
e.Use(middleware.CORS())
e.GET("/color/:value", func(c echo.Context) error {
col, err := color.Parse(c.Param("value"))
if err != nil {
return c.String(400, err.Error())
}
rgb, _ := col.ToRGB()
xy, _ := col.ToXY()
hs, _ := col.ToHS()
return c.JSON(200, color.Color{
K: col.K,
RGB: rgb.RGB,
HS: hs.HS,
XY: xy.XY,
})
})
e.GET("/state", func(c echo.Context) error {
svc.mu.Lock()
data := svc.data
svc.mu.Unlock()
return c.JSON(200, data)
})
e.GET("/subscribe", func(c echo.Context) error {
ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
if err != nil {
return err
}
sub := make(chan uistate.Patch, 64)
svc.addSub(sub)
defer svc.removeSub(sub)
defer ws.Close()
lastSent := time.Now()
patches := make([]uistate.Patch, 0, 128)
for {
patch := <-sub
since := time.Now().Sub(lastSent)
patches = append(patches[:0], patch)
if since < time.Millisecond*950 {
waitCh := time.NewTimer(time.Millisecond*1000 - since)
waiting := true
for waiting {
select {
case patch, ok := <-sub:
patches = append(patches, patch)
waiting = ok
case <-waitCh.C:
waiting = false
}
}
}
err := ws.WriteJSON(patches)
if err != nil {
break
}
lastSent = time.Now()
}
return nil
})
e.GET("/subscribe-simple", func(c echo.Context) error {
type ChangedPatch struct {
Devices map[string]*uistate.Device `json:"devices"`
Assignments map[uuid.UUID]*uistate.Assignment `json:"assignments"`
Scripts map[string][]script.Line `json:"scripts"`
Triggers map[uuid.UUID]*script.Trigger `json:"triggers"`
Full *uistate.Data `json:"full,omitempty"`
}
ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
if err != nil {
return err
}
sub := make(chan uistate.Patch, 64)
svc.addSub(sub)
defer svc.removeSub(sub)
defer ws.Close()
svc.mu.Lock()
patch := ChangedPatch{Full: gentools.Ptr(svc.data.Copy())}
svc.mu.Unlock()
err = ws.WriteJSON(patch)
if err != nil {
return err
}
patches := make([]uistate.Patch, 0, 128)
for {
patch := <-sub
patches = append(patches[:0], patch)
waitCh := time.After(time.Millisecond * 330)
waiting := true
for waiting {
select {
case patch, ok := <-sub:
patches = append(patches, patch)
waiting = ok
case <-waitCh:
waiting = false
}
}
statePatch := ChangedPatch{
Devices: make(map[string]*uistate.Device),
Assignments: make(map[uuid.UUID]*uistate.Assignment),
Scripts: make(map[string][]script.Line),
Triggers: make(map[uuid.UUID]*script.Trigger),
Full: nil,
}
svc.mu.Lock()
data := svc.data.Copy()
svc.mu.Unlock()
for _, patch := range patches {
if patch.Device != nil {
if patch.Device.Delete {
statePatch.Devices[patch.Device.ID] = nil
} else {
statePatch.Devices[patch.Device.ID] = gentools.Ptr(data.Devices[patch.Device.ID])
}
}
if patch.Assignment != nil {
if patch.Assignment.Delete {
statePatch.Assignments[patch.Assignment.ID] = nil
} else {
statePatch.Assignments[patch.Assignment.ID] = gentools.Ptr(data.Assignments[patch.Assignment.ID])
}
}
if patch.Script != nil {
if len(patch.Script.Lines) > 0 {
statePatch.Scripts[patch.Script.Name] = data.Scripts[patch.Script.Name]
} else {
statePatch.Scripts[patch.Script.Name] = nil
}
}
if patch.Trigger != nil {
if patch.Trigger.Delete {
statePatch.Triggers[patch.Trigger.ID] = nil
} else {
statePatch.Triggers[patch.Trigger.ID] = gentools.Ptr(data.Triggers[patch.Trigger.ID])
}
}
}
err := ws.WriteJSON(statePatch)
if err != nil {
break
}
}
return nil
})
e.POST("/command", func(c echo.Context) error {
var input commandInput
err := c.Bind(&input)
if err != nil {
return err
}
svc.mu.Lock()
bus := svc.bus
svc.mu.Unlock()
if bus == nil {
return c.String(413, "Waiting for bus")
}
switch {
case input.Assign != nil:
bus.RunCommand(commands.Assign{
ID: nil,
Match: input.Assign.Match,
Effect: input.Assign.Effect.Effect,
})
case input.PairDevice != nil:
bus.RunCommand(*input.PairDevice)
case input.ConnectDevice != nil:
bus.RunCommand(*input.ConnectDevice)
case input.ForgetDevice != nil:
bus.RunCommand(*input.ForgetDevice)
case input.SearchDevices != nil:
bus.RunCommand(*input.SearchDevices)
case input.AddAlias != nil:
bus.RunCommand(*input.AddAlias)
case input.RemoveAlias != nil:
bus.RunCommand(*input.RemoveAlias)
case input.UpdateScript != nil:
bus.RunCommand(*input.UpdateScript)
case input.ExecuteScript != nil:
bus.RunCommand(*input.ExecuteScript)
case input.UpdateTrigger != nil:
if input.UpdateTrigger.ID == zeroUUID {
input.UpdateTrigger.ID = uuid.New()
}
if input.UpdateTrigger.ScriptPre == nil {
input.UpdateTrigger.ScriptPre = make([]script.Line, 0)
}
if input.UpdateTrigger.ScriptPost == nil {
input.UpdateTrigger.ScriptPost = make([]script.Line, 0)
}
bus.RunCommand(*input.UpdateTrigger)
case input.DeleteTrigger != nil:
bus.RunCommand(*input.DeleteTrigger)
default:
return c.String(400, "No supported command found in input")
}
return c.JSON(200, input)
})
subFS, err := fs.Sub(lucifer3.FrontendFS, "frontend/build")
if err != nil {
return nil, err
}
e.StaticFS("/", subFS)
listener, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
e.Listener = listener
go func() {
err := e.Start(addr)
if err != nil {
log.Fatalln("Failed to listen to webserver")
}
}()
return svc, nil
}
type service struct {
mu sync.Mutex
data uistate.Data
bus *lucifer3.EventBus
subs []chan uistate.Patch
}
func (s *service) Active() bool {
return true
}
func (s *service) HandleEvent(bus *lucifer3.EventBus, event lucifer3.Event) {
switch event := event.(type) {
case events.Started:
s.mu.Lock()
s.bus = bus
s.mu.Unlock()
case uistate.Patch:
s.mu.Lock()
s.data = s.data.WithPatch(event)
subs := s.subs
s.mu.Unlock()
for _, sub := range subs {
select {
case sub <- event:
default:
}
}
}
}
func (s *service) addSub(ch chan uistate.Patch) {
s.mu.Lock()
defer s.mu.Unlock()
s.subs = append(s.subs, ch)
}
func (s *service) removeSub(ch chan uistate.Patch) {
s.mu.Lock()
defer s.mu.Unlock()
s.subs = append([]chan uistate.Patch{}, s.subs...)
for i, sub := range s.subs {
if sub == ch {
s.subs = append(s.subs[:i], s.subs[i+1:]...)
break
}
}
}
type commandInput struct {
Assign *assignInput `json:"assign,omitempty"`
AddAlias *commands.AddAlias `json:"addAlias,omitempty"`
RemoveAlias *commands.RemoveAlias `json:"removeAlias,omitempty"`
PairDevice *commands.PairDevice `json:"pairDevice,omitempty"`
ConnectDevice *commands.ConnectDevice `json:"connectDevice,omitempty"`
SearchDevices *commands.SearchDevices `json:"searchDevices,omitempty"`
ForgetDevice *commands.ForgetDevice `json:"forgetDevice,omitempty"`
UpdateScript *script.Update `json:"updateScript,omitempty"`
ExecuteScript *script.Execute `json:"executeScript,omitempty"`
UpdateTrigger *script.UpdateTrigger `json:"updateTrigger,omitempty"`
DeleteTrigger *script.DeleteTrigger `json:"deleteTrigger,omitempty"`
}
type assignInput struct {
Match string `json:"match"`
Effect effects.Serializable `json:"effect"`
}