basic HTTP api and uistate.
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
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/services/uistate"
|
||||
"github.com/labstack/echo/v4"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func New(addr string) (lucifer3.Service, error) {
|
||||
svc := &service{}
|
||||
|
||||
e := echo.New()
|
||||
e.GET("/state", func(c echo.Context) error {
|
||||
svc.mu.Lock()
|
||||
data := svc.data
|
||||
svc.mu.Unlock()
|
||||
|
||||
return c.JSON(200, data)
|
||||
})
|
||||
|
||||
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.ForgetDevice != nil:
|
||||
bus.RunCommand(*input.ForgetDevice)
|
||||
case input.SearchDevices != nil:
|
||||
bus.RunCommand(*input.SearchDevices)
|
||||
default:
|
||||
return c.String(400, "No supported command found in input")
|
||||
}
|
||||
|
||||
return c.JSON(200, input)
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
s.mu.Unlock()
|
||||
|
||||
// TODO: Broadcast websockets
|
||||
}
|
||||
}
|
||||
|
||||
type commandInput struct {
|
||||
Assign *assignInput `json:"assign,omitempty"`
|
||||
PairDevice *commands.PairDevice `json:"pairDevice,omitempty"`
|
||||
SearchDevices *commands.SearchDevices `json:"searchDevices,omitempty"`
|
||||
ForgetDevice *commands.ForgetDevice `json:"forgetDevice,omitempty"`
|
||||
}
|
||||
|
||||
type assignInput struct {
|
||||
Match string `json:"match"`
|
||||
Effect effects.Serializable `json:"effect"`
|
||||
}
|
||||
@@ -292,7 +292,7 @@ func (b *Bridge) makeCongruentLoop(ctx context.Context) {
|
||||
}
|
||||
|
||||
// Make sure this loop doesn't spam too hard
|
||||
rateLimit := time.After(time.Second / 10)
|
||||
rateLimit := time.After(time.Second / 15)
|
||||
|
||||
// Take states
|
||||
b.mu.Lock()
|
||||
|
||||
@@ -116,6 +116,7 @@ func (q *Queries) DeleteDeviceInfoLike(ctx context.Context, id string) error {
|
||||
const insertDeviceAlias = `-- name: InsertDeviceAlias :exec
|
||||
INSERT INTO device_alias (id, alias)
|
||||
VALUES (?, ?)
|
||||
ON DUPLICATE KEY UPDATE alias=alias
|
||||
`
|
||||
|
||||
type InsertDeviceAliasParams struct {
|
||||
|
||||
@@ -17,7 +17,8 @@ FROM device_alias;
|
||||
|
||||
-- name: InsertDeviceAlias :exec
|
||||
INSERT INTO device_alias (id, alias)
|
||||
VALUES (?, ?);
|
||||
VALUES (?, ?)
|
||||
ON DUPLICATE KEY UPDATE alias=alias;
|
||||
|
||||
-- name: ReplaceDeviceAuth :exec
|
||||
REPLACE INTO device_auth (id, api_key, extras)
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package uistate
|
||||
|
||||
import (
|
||||
"git.aiterp.net/lucifer3/server/device"
|
||||
"git.aiterp.net/lucifer3/server/effects"
|
||||
"git.aiterp.net/lucifer3/server/events"
|
||||
"git.aiterp.net/lucifer3/server/internal/gentools"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Data struct {
|
||||
Devices map[string]Device `json:"devices"`
|
||||
Assignments map[uuid.UUID]Assignment `json:"assignments"`
|
||||
}
|
||||
|
||||
func (d *Data) WithPatch(patches ...Patch) Data {
|
||||
newData := d.Copy()
|
||||
for _, patch := range patches {
|
||||
if patch.Device != nil {
|
||||
pd := d.ensureDevice(patch.Device.ID)
|
||||
|
||||
gentools.ApplyUpdateSlice(&pd.Aliases, patch.Device.SetAliases)
|
||||
gentools.ApplyUpdate(&pd.Name, patch.Device.Name)
|
||||
gentools.ApplyUpdatePtr(&pd.HWState, patch.Device.HWState)
|
||||
gentools.ApplyUpdatePtr(&pd.HWMetadata, patch.Device.HWMetadata)
|
||||
gentools.ApplyUpdatePtr(&pd.DesiredState, patch.Device.DesiredState)
|
||||
gentools.ApplyUpdatePtr(&pd.Assignment, patch.Device.Assignment)
|
||||
|
||||
if patch.Device.AddAlias != nil {
|
||||
pd.Aliases = append(pd.Aliases[:0:0], pd.Aliases...)
|
||||
pd.Aliases = append(pd.Aliases, *patch.Device.AddAlias)
|
||||
}
|
||||
if patch.Device.RemoveAlias != nil {
|
||||
for i, alias := range pd.Aliases {
|
||||
if alias == *patch.Device.RemoveAlias {
|
||||
pd.Aliases = append(pd.Aliases[:0:0], pd.Aliases...)
|
||||
pd.Aliases = append(pd.Aliases[:i], pd.Aliases[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if patch.Device.ClearAssignment {
|
||||
pd.Assignment = nil
|
||||
}
|
||||
|
||||
if patch.Device.Delete {
|
||||
delete(newData.Devices, pd.ID)
|
||||
} else {
|
||||
newData.Devices[pd.ID] = pd
|
||||
}
|
||||
}
|
||||
|
||||
if patch.Assignment != nil {
|
||||
pa := d.ensureAssignment(patch.Assignment.ID)
|
||||
gentools.ApplyUpdatePtr(&pa.Effect, patch.Assignment.Effect)
|
||||
if patch.Assignment.AddDeviceID != nil {
|
||||
pa.DeviceIDs = append(pa.DeviceIDs[:0:0], pa.DeviceIDs...)
|
||||
pa.DeviceIDs = append(pa.DeviceIDs, *patch.Assignment.AddDeviceID)
|
||||
}
|
||||
if patch.Assignment.RemoveDeviceID != nil {
|
||||
for i, id := range pa.DeviceIDs {
|
||||
if id == *patch.Assignment.RemoveDeviceID {
|
||||
pa.DeviceIDs = append(pa.DeviceIDs[:0:0], pa.DeviceIDs...)
|
||||
pa.DeviceIDs[i] = ""
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if patch.Assignment.Delete {
|
||||
delete(newData.Assignments, pa.ID)
|
||||
} else {
|
||||
newData.Assignments[pa.ID] = pa
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newData
|
||||
}
|
||||
|
||||
func (d *Data) Copy() Data {
|
||||
return Data{
|
||||
Devices: gentools.CopyMap(d.Devices),
|
||||
Assignments: gentools.CopyMap(d.Assignments),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Data) ensureDevice(id string) Device {
|
||||
if device, ok := d.Devices[id]; ok {
|
||||
return device
|
||||
} else {
|
||||
return Device{ID: id}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Data) ensureAssignment(id uuid.UUID) Assignment {
|
||||
if assignment, ok := d.Assignments[id]; ok {
|
||||
return assignment
|
||||
} else {
|
||||
return Assignment{ID: id}
|
||||
}
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
HWMetadata *events.HardwareMetadata `json:"hwMetadata"`
|
||||
HWState *events.HardwareState `json:"hwState"`
|
||||
DesiredState *device.State `json:"desiredState"`
|
||||
Aliases []string `json:"aliases"`
|
||||
Assignment *uuid.UUID `json:"assignment"`
|
||||
}
|
||||
|
||||
type Assignment struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
DeviceIDs []string `json:"deviceIds"`
|
||||
Effect *effects.Serializable `json:"effect"`
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package uistate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.aiterp.net/lucifer3/server/device"
|
||||
"git.aiterp.net/lucifer3/server/effects"
|
||||
"git.aiterp.net/lucifer3/server/events"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Patch struct {
|
||||
Assignment *AssignmentPatch `json:"assignment,omitempty"`
|
||||
Device *DevicePatch `json:"device,omitempty"`
|
||||
}
|
||||
|
||||
func (e Patch) EventDescription() string {
|
||||
if e.Device != nil {
|
||||
switch {
|
||||
case e.Device.DesiredState != nil:
|
||||
return fmt.Sprintf("uistate.Patch(device=%s, desired state)", e.Device.ID)
|
||||
default:
|
||||
return fmt.Sprintf("uistate.Patch(device=%s)", e.Device.ID)
|
||||
}
|
||||
} else if e.Assignment != nil {
|
||||
return fmt.Sprintf("uistate.Patch(assignment=%s)", e.Assignment.ID)
|
||||
} else {
|
||||
return "uistate.Patch"
|
||||
}
|
||||
}
|
||||
|
||||
type DevicePatch struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
HWMetadata *events.HardwareMetadata `json:"hwMetadata,omitempty"`
|
||||
HWState *events.HardwareState `json:"hwState,omitempty"`
|
||||
DesiredState *device.State `json:"desiredState,omitempty"`
|
||||
SetAliases []string `json:"setAliases,omitempty"`
|
||||
AddAlias *string `json:"addAlias,omitempty"`
|
||||
RemoveAlias *string `json:"removeAlias,omitempty"`
|
||||
Assignment *uuid.UUID `json:"assignment,omitempty"`
|
||||
ClearAssignment bool `json:"clearAssignment,omitempty"`
|
||||
Delete bool `json:"delete,omitempty"`
|
||||
}
|
||||
|
||||
type AssignmentPatch struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
AddDeviceID *string `json:"addDeviceId"`
|
||||
RemoveDeviceID *string `json:"removeDeviceId"`
|
||||
Effect *effects.Serializable `json:"effect"`
|
||||
Delete bool `json:"delete"`
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package uistate
|
||||
|
||||
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/gentools"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func NewService() lucifer3.ActiveService {
|
||||
return &service{}
|
||||
}
|
||||
|
||||
type service struct {
|
||||
mu sync.Mutex
|
||||
data Data
|
||||
listener []chan Patch
|
||||
}
|
||||
|
||||
func (s *service) Active() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *service) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Command) {
|
||||
var patches []Patch
|
||||
|
||||
switch command := command.(type) {
|
||||
case commands.SetState:
|
||||
patches = []Patch{{Device: &DevicePatch{ID: command.ID, DesiredState: &command.State}}}
|
||||
case commands.SetStateBatch:
|
||||
for id, state := range command {
|
||||
patches = []Patch{{Device: &DevicePatch{ID: id, DesiredState: gentools.ShallowCopy(&state)}}}
|
||||
}
|
||||
}
|
||||
|
||||
if len(patches) > 0 {
|
||||
s.mu.Lock()
|
||||
s.data = s.data.WithPatch(patches...)
|
||||
s.mu.Unlock()
|
||||
|
||||
for _, patch := range patches {
|
||||
bus.RunEvent(patch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) HandleEvent(bus *lucifer3.EventBus, event lucifer3.Event) {
|
||||
var patches []Patch
|
||||
|
||||
switch event := event.(type) {
|
||||
case events.AliasAdded:
|
||||
patches = []Patch{{Device: &DevicePatch{ID: event.ID, AddAlias: &event.Alias}}}
|
||||
case events.AliasRemoved:
|
||||
patches = []Patch{{Device: &DevicePatch{ID: event.ID, RemoveAlias: &event.Alias}}}
|
||||
case events.HardwareState:
|
||||
patches = []Patch{{Device: &DevicePatch{ID: event.ID, HWState: &event}}}
|
||||
case events.HardwareMetadata:
|
||||
patches = []Patch{{Device: &DevicePatch{ID: event.ID, HWMetadata: &event}}}
|
||||
case events.AssignmentCreated:
|
||||
patches = []Patch{{Assignment: &AssignmentPatch{
|
||||
ID: event.ID,
|
||||
Effect: &effects.Serializable{Effect: event.Effect},
|
||||
}}}
|
||||
case events.AssignmentRemoved:
|
||||
patches = []Patch{{Assignment: &AssignmentPatch{
|
||||
ID: event.ID,
|
||||
Delete: true,
|
||||
}}}
|
||||
case events.DeviceAssigned:
|
||||
// Un-assign from current assignment (if any)
|
||||
if d, ok := s.data.Devices[event.DeviceID]; ok && d.Assignment != nil {
|
||||
patches = append(patches, Patch{Assignment: &AssignmentPatch{
|
||||
ID: *d.Assignment,
|
||||
RemoveDeviceID: &d.ID,
|
||||
}})
|
||||
}
|
||||
|
||||
// Assign to current assignment (if it's not cleared)
|
||||
if event.AssignmentID != nil {
|
||||
patches = append(patches, Patch{Assignment: &AssignmentPatch{
|
||||
ID: *event.AssignmentID,
|
||||
AddDeviceID: &event.DeviceID,
|
||||
}})
|
||||
}
|
||||
|
||||
// Always set the assignment
|
||||
patches = append(patches, Patch{Device: &DevicePatch{
|
||||
ID: event.DeviceID,
|
||||
Assignment: gentools.ShallowCopy(event.AssignmentID),
|
||||
ClearAssignment: event.AssignmentID == nil,
|
||||
}})
|
||||
}
|
||||
|
||||
if len(patches) > 0 {
|
||||
s.mu.Lock()
|
||||
s.data = s.data.WithPatch(patches...)
|
||||
s.mu.Unlock()
|
||||
|
||||
for _, patch := range patches {
|
||||
bus.RunEvent(patch)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user