Browse Source

basic HTTP api and uistate.

beelzebub
Gisle Aune 1 year ago
parent
commit
fe5bfc1efc
  1. 1
      .gitignore
  2. 17
      bus.go
  3. 70
      cmd/lucifer4-server/main.go
  4. 13
      go.mod
  5. 29
      go.sum
  6. 16
      internal/color/color.go
  7. 66
      internal/gentools/update.go
  8. 112
      services/httpapiv1/service.go
  9. 2
      services/hue/bridge.go
  10. 1
      services/mysqldb/mysqlgen/device.sql.go
  11. 3
      services/mysqldb/queries/device.sql
  12. 118
      services/uistate/data.go
  13. 51
      services/uistate/patch.go
  14. 105
      services/uistate/service.go

1
.gitignore

@ -1,2 +1,3 @@
.idea
.vscode
.env

17
bus.go

@ -2,9 +2,7 @@ package lucifer3
import (
"fmt"
"strings"
"sync"
"sync/atomic"
)
type ServiceKey struct{}
@ -65,20 +63,7 @@ func (b *EventBus) JoinPrivileged(service ActiveService) {
}
func (b *EventBus) RunCommand(command Command) {
if cd := command.CommandDescription(); !strings.HasPrefix(cd, "SetState") {
if setStates := atomic.LoadInt32(&b.setStates); setStates > 0 {
fmt.Println("[INFO]", setStates, "SetStates commands hidden.")
atomic.AddInt32(&b.setStates, -setStates)
}
fmt.Println("[COMMAND]", cd)
} else {
if atomic.AddInt32(&b.setStates, 1) >= 1000 {
fmt.Println("[INFO] 1000 SetStates commands hidden.")
atomic.AddInt32(&b.setStates, -1000)
}
}
fmt.Println("[COMMAND]", command.CommandDescription())
b.send(serviceMessage{command: command})
}

70
cmd/lucifer4-server/main.go

@ -0,0 +1,70 @@
package main
import (
lucifer3 "git.aiterp.net/lucifer3/server"
"git.aiterp.net/lucifer3/server/events"
"git.aiterp.net/lucifer3/server/services"
"git.aiterp.net/lucifer3/server/services/httpapiv1"
"git.aiterp.net/lucifer3/server/services/hue"
"git.aiterp.net/lucifer3/server/services/mysqldb"
"git.aiterp.net/lucifer3/server/services/nanoleaf"
"git.aiterp.net/lucifer3/server/services/uistate"
"log"
"os"
"strconv"
"time"
)
func main() {
bus := lucifer3.EventBus{}
resolver := services.NewResolver()
sceneMap := services.NewSceneMap(resolver)
database, err := mysqldb.Connect(
env("LUCIFER4_DB_HOST"),
envInt("LUCIFER4_DB_PORT"),
env("LUCIFER4_DB_USER"),
env("LUCIFER4_DB_PASSWORD"),
env("LUCIFER4_DB_SCHEMA"),
)
if err != nil {
log.Fatalln("Database failed", err)
}
httpAPI, err := httpapiv1.New(env("LUCIFER4_HTTP_LISTEN"))
if err != nil {
log.Fatalln("HTTP Listen failed", err)
}
bus.JoinPrivileged(resolver)
bus.JoinPrivileged(sceneMap)
bus.Join(services.NewEffectEnforcer(resolver, sceneMap))
bus.Join(nanoleaf.NewService())
bus.Join(hue.NewService())
bus.Join(uistate.NewService())
bus.Join(database)
bus.Join(httpAPI)
bus.RunEvent(events.Started{})
time.Sleep(time.Hour * 16)
}
func env(key string) string {
value := os.Getenv(key)
if value == "" {
log.Fatalln("Expected env:", key)
}
return value
}
func envInt(key string) int {
value, err := strconv.Atoi(os.Getenv(key))
if err != nil {
log.Fatalln("Expected numeric env:", key)
}
return value
}

13
go.mod

@ -17,9 +17,16 @@ require (
github.com/dustin/go-coap v0.0.0-20170214053734-ddcc80675fa4 // indirect
github.com/eriklupander/dtls v0.0.0-20190304211642-b36018226359 // indirect
github.com/golang/protobuf v1.3.1 // indirect
golang.org/x/net v0.0.0-20190311183353-d8887717615a // indirect
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
golang.org/x/text v0.3.0 // indirect
github.com/labstack/echo/v4 v4.10.0 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/crypto v0.2.0 // indirect
golang.org/x/net v0.4.0 // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/text v0.5.0 // indirect
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 // indirect
google.golang.org/grpc v1.21.1 // indirect
)

29
go.sum

@ -33,9 +33,19 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/labstack/echo/v4 v4.10.0 h1:5CiyngihEO4HXsz3vVsJn7f8xAlWwRr3aY6Ih280ZKA=
github.com/labstack/echo/v4 v4.10.0/go.mod h1:S/T/5fy/GigaXnHTkh0ZGe4LpkkQysvRjFMSUTkDRNQ=
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -54,10 +64,20 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE=
golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
@ -65,10 +85,18 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
@ -81,4 +109,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

16
internal/color/color.go

@ -1,6 +1,7 @@
package color
import (
"encoding/json"
"fmt"
"github.com/lucasb-eyer/go-colorful"
"math"
@ -15,6 +16,21 @@ type Color struct {
XY *XY `json:"xy,omitempty"`
}
func (col *Color) MarshalJSON() ([]byte, error) {
return json.Marshal(col.String())
}
func (col *Color) UnmarshalJSON(bytes []byte) error {
var s string
err := json.Unmarshal(bytes, &s)
if err != nil {
return err
}
*col, err = Parse(s)
return err
}
func (col *Color) IsHueSat() bool {
return col.HS != nil
}

66
internal/gentools/update.go

@ -0,0 +1,66 @@
package gentools
func ApplyUpdate[T any](dst *T, value *T) {
if value != nil {
*dst = *value
}
}
func ApplyUpdatePtr[T any](dst **T, value *T) {
if value != nil {
*dst = ShallowCopy(value)
}
}
func ApplyUpdateSlice[T any](dst *[]T, value []T) {
if value != nil {
*dst = append([]T{}, value...)
}
}
func ApplyUpdateNonZero[T comparable](dst *T, value *T) {
var zero T
if value != nil && *value != zero {
*dst = *value
}
}
func ApplyUpdateNilZero[T comparable](dst **T, value *T) {
if value != nil {
var zero T
if *value == zero {
*dst = nil
} else {
valueCopy := *value
*dst = &valueCopy
}
}
}
func ApplyMapUpdate[K comparable, V any](dst *map[K]V, src map[K]*V) {
if *dst == nil {
dst = &map[K]V{}
}
for key, value := range src {
if value != nil {
(*dst)[key] = *value
} else {
delete(*dst, key)
}
}
}
func ApplyUpdateMapNilZero[K comparable, V comparable](dst *map[K]V, src map[K]V) {
var zero V
if *dst == nil {
dst = &map[K]V{}
}
for key, value := range src {
if value != zero {
(*dst)[key] = value
} else {
delete(*dst, key)
}
}
}

112
services/httpapiv1/service.go

@ -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"`
}

2
services/hue/bridge.go

@ -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()

1
services/mysqldb/mysqlgen/device.sql.go

@ -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 {

3
services/mysqldb/queries/device.sql

@ -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)

118
services/uistate/data.go

@ -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"`
}

51
services/uistate/patch.go

@ -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"`
}

105
services/uistate/service.go

@ -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)
}
}
}
Loading…
Cancel
Save