Browse Source

small fixed, device repo and some device api

pull/1/head
Gisle Aune 3 years ago
parent
commit
26fcc1f314
  1. 25
      app/api/bridges.go
  2. 123
      app/api/devices.go
  3. 9
      app/config/driver.go
  4. 2
      app/config/repo.go
  5. 4
      app/server.go
  6. 34
      app/services/bridges.go
  7. 12
      app/services/events.go
  8. 60
      app/services/publish.go
  9. 95
      app/services/synclights.go
  10. 8
      cmd/goose/main.go
  11. 7
      go.mod
  12. 17
      go.sum
  13. 8
      internal/mysql/bridgerepo.go
  14. 313
      internal/mysql/devicerepo.go
  15. 17
      models/device.go
  16. 1
      models/shared.go
  17. 5
      scripts/20210522140146_device.sql
  18. 2
      scripts/20210522140148_device_state.sql
  19. 16
      scripts/20210918105052_device_tag.sql

25
app/api/bridges.go

@ -1,6 +1,7 @@
package api
import (
"fmt"
"git.aiterp.net/lucifer/new-server/app/config"
"git.aiterp.net/lucifer/new-server/models"
"github.com/gin-gonic/gin"
@ -18,9 +19,11 @@ func Bridges(r gin.IRoutes) {
r.POST("", handler(func(c *gin.Context) (interface{}, error) {
var body struct {
Driver models.DriverKind `json:"driver"`
Address string `json:"address"`
DryRun bool `json:"dryRun"`
Driver models.DriverKind `json:"driver"`
Address string `json:"address"`
Token string `json:"token"`
ForceSave bool `json:"forceSave"`
DryRun bool `json:"dryRun"`
}
err := parseBody(c, &body)
if err != nil {
@ -32,6 +35,22 @@ func Bridges(r gin.IRoutes) {
return nil, err
}
if body.ForceSave {
bridge := models.Bridge{
Name: fmt.Sprintf("Manually saved %s bridge (%s)", body.Driver, body.Address),
Driver: body.Driver,
Address: body.Address,
Token: body.Token,
}
err := config.BridgeRepository().Save(ctxOf(c), &bridge)
if err != nil {
return nil, err
}
return []models.Bridge{bridge}, nil
}
bridges, err := driver.SearchBridge(ctxOf(c), body.Address, body.DryRun)
if err != nil {
return nil, err

123
app/api/devices.go

@ -0,0 +1,123 @@
package api
import (
"context"
"git.aiterp.net/lucifer/new-server/app/config"
"git.aiterp.net/lucifer/new-server/models"
"github.com/gin-gonic/gin"
"log"
"strings"
)
func fetchDevices(ctx context.Context, fetchStr string) ([]models.Device, error) {
if strings.HasPrefix(fetchStr, "tag:") {
return config.DeviceRepository().FetchByReference(ctx, models.RKTag, fetchStr[4:])
} else if strings.HasPrefix(fetchStr, "bridge:") {
return config.DeviceRepository().FetchByReference(ctx, models.RKBridgeID, fetchStr[7:])
} else if fetchStr == "all" {
return config.DeviceRepository().FetchByReference(ctx, models.RKAll, "")
} else {
return config.DeviceRepository().FetchByReference(ctx, models.RKDeviceID, fetchStr)
}
}
func Devices(r gin.IRoutes) {
r.GET("", handler(func(c *gin.Context) (interface{}, error) {
return config.DeviceRepository().FetchByReference(ctxOf(c), models.RKAll, "")
}))
r.GET("/:fetch", handler(func(c *gin.Context) (interface{}, error) {
return fetchDevices(ctxOf(c), c.Param("fetch"))
}))
r.PUT("/:fetch/state", handler(func(c *gin.Context) (interface{}, error) {
state := models.NewDeviceState{}
err := parseBody(c, &state)
if err != nil {
return nil, err
}
devices, err := fetchDevices(ctxOf(c), c.Param("fetch"))
if err != nil {
return nil, err
}
if len(devices) == 0 {
return []models.Device{}, nil
}
for i := range devices {
err := devices[i].SetState(state)
if err != nil {
return nil, err
}
}
config.PublishChannel <- devices
go func() {
for _, device := range devices {
err := config.DeviceRepository().Save(context.Background(), &device)
if err != nil {
log.Println("Failed to save device for state:", err)
continue
}
}
}()
return devices, nil
}))
r.PUT("/:fetch/tags", handler(func(c *gin.Context) (interface{}, error) {
var body struct {
Add []string `json:"add"`
Remove []string `json:"remove"`
}
err := parseBody(c, &body)
if err != nil {
return nil, err
}
devices, err := fetchDevices(ctxOf(c), c.Param("fetch"))
if err != nil {
return nil, err
}
if len(devices) == 0 {
return []models.Device{}, nil
}
for i := range devices {
device := &devices[i]
for _, tag := range body.Add {
found := false
for _, tag2 := range device.Tags {
if tag == tag2 {
found = true
break
}
}
if !found {
device.Tags = append(device.Tags, tag)
}
}
for _, tag := range body.Remove {
index := -1
for i, tag2 := range device.Tags {
if tag == tag2 {
index = i
}
}
device.Tags = append(device.Tags[:index], device.Tags[index+1:]...)
}
err = config.DeviceRepository().Save(ctxOf(c), device)
if err != nil {
return nil, err
}
}
return devices, nil
}))
}

9
app/config/driver.go

@ -10,19 +10,16 @@ import (
)
var dp models.DriverProvider
var dpMutex sync.Mutex
var dpOnce sync.Once
func DriverProvider() models.DriverProvider {
dpMutex.Lock()
defer dpMutex.Unlock()
if dp == nil {
dpOnce.Do(func() {
dp = drivers.DriverMap{
models.DTNanoLeaf: &nanoleaf.Driver{},
models.DTHue: &hue.Driver{},
models.DTLIFX: &lifx.Driver{},
}
}
})
return dp
}

2
app/config/repo.go

@ -30,7 +30,7 @@ func ColorPresetRepository() models.ColorPresetRepository {
func DeviceRepository() models.DeviceRepository {
if dRepo == nil {
panic("panik")
dRepo = &mysql.DeviceRepo{DBX: DBX()}
}
return dRepo

4
app/server.go

@ -11,12 +11,16 @@ import (
func StartServer() {
services.StartEventHandler()
services.StartPublisher()
services.ConnectToBridges()
services.CheckNewDevices()
gin.SetMode(gin.ReleaseMode)
ginny := gin.New()
apiGin := ginny.Group("/api")
api.Bridges(apiGin.Group("/bridges"))
api.Devices(apiGin.Group("/devices"))
api.ColorPresets(apiGin.Group("/color-presets"))
api.DriverKinds(apiGin.Group("/driver-kinds"))
api.Events(apiGin.Group("/events"))

34
app/services/bridges.go

@ -3,11 +3,15 @@ package services
import (
"context"
"git.aiterp.net/lucifer/new-server/app/config"
"git.aiterp.net/lucifer/new-server/models"
"log"
"strconv"
"sync"
"time"
)
var cancelMap = make(map[int]context.CancelFunc, 8)
var cancelMutex sync.Mutex
func ConnectToBridges() {
go func() {
@ -29,7 +33,10 @@ func runConnectToBridges() error {
}
for _, bridge := range bridges {
if cancelMap[bridge.ID] != nil {
cancelMutex.Lock()
isRunning := cancelMap[bridge.ID] != nil
cancelMutex.Unlock()
if isRunning {
continue
}
@ -39,11 +46,30 @@ func runConnectToBridges() error {
}
ctx, cancel := context.WithCancel(context.Background())
err = driver.Run(ctx, bridge, config.EventChannel)
cancelMutex.Lock()
cancelMap[bridge.ID] = cancel
cancelMutex.Unlock()
log.Printf("Running bridge \"%s\" (%d)", bridge.Name, bridge.ID)
go func(bridge models.Bridge, cancel func()) {
savedDevices, err := config.DeviceRepository().FetchByReference(ctx, models.RKBridgeID, strconv.Itoa(bridge.ID))
if err != nil {
log.Println("Failed to fetch devices from db for refresh:", err)
}
err = driver.Publish(ctx, bridge, savedDevices)
if err != nil {
log.Println("Failed to publish devices from db before run:", err)
}
err = driver.Run(ctx, bridge, config.EventChannel)
log.Printf("Bridge \"%s\" (%d) stopped: %s", bridge.Name, bridge.ID, err)
log.Printf("Connected to bridge \"%s\" (%d)", bridge.Name, bridge.ID)
cancelMutex.Lock()
cancel()
cancelMap[bridge.ID] = nil
cancelMutex.Unlock()
}(bridge, cancel)
}
return nil

12
app/services/events.go

@ -2,10 +2,12 @@ package services
import (
"context"
"fmt"
"git.aiterp.net/lucifer/new-server/app/config"
"git.aiterp.net/lucifer/new-server/models"
"log"
"strconv"
"strings"
"time"
)
@ -18,7 +20,7 @@ func StartEventHandler() {
}
}()
// Dispatch an HourChanged event at every hour
// Generate TimeChanged event
go func() {
drift := time.Now().Add(time.Minute).Truncate(time.Minute).Sub(time.Now())
time.Sleep(drift + time.Millisecond * 5)
@ -48,7 +50,12 @@ func handleEvent(event models.Event) {
}
if !X {
log.Println("Unhandled event: " + event.Name)
paramStrings := make([]string, 0, 8)
for key, value := range event.Payload {
paramStrings = append(paramStrings, fmt.Sprintf("%s=%s", key, value))
}
log.Printf("Unhandled event %s(%s)", event.Name, strings.Join(paramStrings, ", "))
return
}
@ -68,7 +75,6 @@ func handleEvent(event models.Event) {
if !handler.MatchesEvent(event, devices) {
continue
}
}
}

60
app/services/publish.go

@ -5,6 +5,8 @@ import (
"git.aiterp.net/lucifer/new-server/app/config"
"git.aiterp.net/lucifer/new-server/models"
"log"
"sync"
"time"
)
func StartPublisher() {
@ -16,32 +18,58 @@ func StartPublisher() {
continue
}
// Emergency solution! Please avoid!
// Send devices not belonging to the first channel separately
bridgeID := devices[0].BridgeID
lists := make(map[int][]models.Device, 4)
for _, device := range devices {
if device.BridgeID != bridgeID {
config.PublishChannel<-[]models.Device{device}
}
lists[device.BridgeID] = append(lists[device.BridgeID], device)
}
bridge, err := config.BridgeRepository().Find(ctx, devices[0].BridgeID)
ctx, cancel := context.WithTimeout(ctx, time.Second * 30)
bridges, err := config.BridgeRepository().FetchAll(ctx)
if err != nil {
log.Println("Publishing error (1): " + err.Error())
continue
}
driver, err := config.DriverProvider().Provide(bridge.Driver)
if err != nil {
log.Println("Publishing error (2): " + err.Error())
continue
}
wg := sync.WaitGroup{}
for _, devices := range lists {
wg.Add(1)
err = driver.Publish(ctx, bridge, devices)
if err != nil {
log.Println("Publishing error (3): " + err.Error())
continue
go func(devices []models.Device) {
defer wg.Done()
var bridge models.Bridge
for _, bridge2 := range bridges {
if bridge2.ID == devices[0].BridgeID {
bridge = bridge2
}
}
if bridge.ID == 0 {
log.Println("Unknown bridge")
}
bridge, err := config.BridgeRepository().Find(ctx, devices[0].BridgeID)
if err != nil {
log.Println("Publishing error (1): " + err.Error())
return
}
driver, err := config.DriverProvider().Provide(bridge.Driver)
if err != nil {
log.Println("Publishing error (2): " + err.Error())
return
}
err = driver.Publish(ctx, bridge, devices)
if err != nil {
log.Println("Publishing error (3): " + err.Error())
return
}
}(devices)
}
wg.Wait()
cancel()
}
}()
}

95
app/services/synclights.go

@ -0,0 +1,95 @@
package services
import (
"context"
"git.aiterp.net/lucifer/new-server/app/config"
"git.aiterp.net/lucifer/new-server/models"
"log"
"strconv"
"time"
)
func CheckNewDevices() {
go func() {
// Wait a bit before the first to let bridges connect.
time.Sleep(time.Second * 5)
err := checkNewDevices()
if err != nil {
log.Println("Failed to sync lights:", err)
}
for range time.NewTicker(time.Second * 30).C {
err := checkNewDevices()
if err != nil {
log.Println("Failed to sync lights:", err)
}
}
}()
}
func checkNewDevices() error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*27)
defer cancel()
bridges, err := config.BridgeRepository().FetchAll(ctx)
if err != nil {
return err
}
for _, bridge := range bridges {
driver, err := config.DriverProvider().Provide(bridge.Driver)
if err != nil {
log.Println("Unknown/unsupported driver:", bridge.Driver)
continue
}
savedDevices, err := config.DeviceRepository().FetchByReference(ctx, models.RKBridgeID, strconv.Itoa(bridge.ID))
if err != nil {
log.Println("Failed to list devices from db:", err)
continue
}
driverDevices, err := driver.ListDevices(ctx, bridge)
if err != nil {
log.Println("Failed to list devices from driver:", err)
continue
}
foundNewDevices := false
SaveLoop:
for _, driverDevice := range driverDevices {
for _, savedDevice := range savedDevices {
if savedDevice.InternalID == driverDevice.InternalID {
continue SaveLoop
}
}
log.Println("Saving new device", driverDevice.InternalID)
err := config.DeviceRepository().Save(ctx, &driverDevice)
if err != nil {
log.Println("Failed to save device:", err)
continue
}
foundNewDevices = true
}
// If new devices were found, publish them so that the driver can be set up.
if foundNewDevices {
savedDevices, err := config.DeviceRepository().FetchByReference(ctx, models.RKBridgeID, strconv.Itoa(bridge.ID))
if err != nil {
log.Println("Failed to fetch devices from db second time:", err)
continue
}
err = driver.Publish(ctx, bridge, savedDevices)
if err != nil {
log.Println("Failed to list devices from db:", err)
continue
}
}
}
return nil
}

8
cmd/goose/main.go

@ -4,6 +4,7 @@ import (
"git.aiterp.net/lucifer/new-server/app/config"
"github.com/pressly/goose"
"log"
"os"
"time"
)
@ -19,7 +20,12 @@ func main() {
log.Fatal(err)
}
err = goose.Run("up", db, "./scripts")
cmd := os.Getenv("GOOSE_COMMAND")
if cmd == "" {
cmd = "up"
}
err = goose.Run(cmd, db, "./scripts")
if err != nil {
log.Fatal(err)
}

7
go.mod

@ -3,14 +3,15 @@ module git.aiterp.net/lucifer/new-server
go 1.16
require (
github.com/Masterminds/squirrel v1.5.0
github.com/gin-gonic/gin v1.7.1
github.com/go-sql-driver/mysql v1.6.0
github.com/jmoiron/sqlx v1.3.4
github.com/lib/pq v1.10.2 // indirect
github.com/lib/pq v1.10.3 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pressly/goose v2.7.0+incompatible
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/crypto v0.0.0-20210915214749-c084706c2272 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
)

17
go.sum

@ -1,3 +1,5 @@
github.com/Masterminds/squirrel v1.5.0 h1:JukIZisrUXadA9pl3rMkjhiamxiB0cXiu+HGp/Y8cY8=
github.com/Masterminds/squirrel v1.5.0/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -23,11 +25,15 @@ github.com/jmoiron/sqlx v1.3.4 h1:wv+0IJZfL5z0uZoUjlpKgHkgaFSYD+r9CfrXjEXsO7w=
github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg=
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
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/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
@ -46,6 +52,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/pressly/goose v2.7.0+incompatible h1:PWejVEv07LCerQEzMMeAtjuyCKbyprZ/LBa6K5P0OCQ=
github.com/pressly/goose v2.7.0+incompatible/go.mod h1:m+QHWCqxR3k8D9l7qfzuC/djtlfzxr34mozWDYEu1z8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@ -55,8 +62,8 @@ github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210915214749-c084706c2272 h1:3erb+vDS8lU1sxfDHF4/hhWyaXnhIaO+7RgL4fDZORA=
golang.org/x/crypto v0.0.0-20210915214749-c084706c2272/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
@ -66,6 +73,8 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=

8
internal/mysql/bridgerepo.go

@ -11,10 +11,10 @@ type BridgeRepo struct {
}
func (b *BridgeRepo) Find(ctx context.Context, id int) (models.Bridge, error) {
var bridge models.Bridge
var bridge models.Bridge
err := b.DBX.GetContext(ctx, &bridge, "SELECT * FROM bridge WHERE id = ?", id)
if err != nil {
return models.Bridge{}, dbErr(err)
if err != nil {
return models.Bridge{}, dbErr(err)
}
return bridge, nil
@ -22,7 +22,7 @@ func (b *BridgeRepo) Find(ctx context.Context, id int) (models.Bridge, error) {
func (b *BridgeRepo) FetchAll(ctx context.Context) ([]models.Bridge, error) {
bridges := make([]models.Bridge, 0, 8)
err := b.DBX.GetContext(ctx, &bridges, "SELECT * FROM bridge")
err := b.DBX.SelectContext(ctx, &bridges, "SELECT * FROM bridge")
if err != nil {
return nil, dbErr(err)
}

313
internal/mysql/devicerepo.go

@ -0,0 +1,313 @@
package mysql
import (
"context"
"encoding/json"
"git.aiterp.net/lucifer/new-server/models"
sq "github.com/Masterminds/squirrel"
"github.com/jmoiron/sqlx"
"log"
"strings"
)
type deviceRecord struct {
ID int `db:"id"`
BridgeID int `db:"bridge_id"`
InternalID string `db:"internal_id"`
Icon string `db:"icon"`
Name string `db:"name"`
Capabilities string `db:"capabilities"`
ButtonNames string `db:"button_names"`
}
type deviceStateRecord struct {
DeviceID int `db:"device_id"`
Hue float64 `db:"hue"`
Saturation float64 `db:"saturation"`
Kelvin int `db:"kelvin"`
Power bool `db:"power"`
Intensity float64 `db:"intensity"`
}
type devicePropertyRecord struct {
DeviceID int `db:"device_id"`
Key string `db:"prop_key"`
Value string `db:"prop_value"`
IsUser bool `db:"is_user"`
}
type deviceTagRecord struct {
DeviceID int `db:"device_id"`
TagName string `db:"tag_name"`
}
type DeviceRepo struct {
DBX *sqlx.DB
}
func (r *DeviceRepo) Find(ctx context.Context, id int) (*models.Device, error) {
var device deviceRecord
err := r.DBX.GetContext(ctx, &device, "SELECT * FROM device WHERE id = ?", id)
if err != nil {
return nil, dbErr(err)
}
return r.populateOne(ctx, device)
}
func (r *DeviceRepo) FetchByReference(ctx context.Context, kind models.ReferenceKind, value string) ([]models.Device, error) {
var err error
records := make([]deviceRecord, 0, 8)
switch kind {
case models.RKDeviceID:
err = r.DBX.SelectContext(ctx, &records, "SELECT * FROM device WHERE id=?", value)
case models.RKBridgeID:
err = r.DBX.SelectContext(ctx, &records, "SELECT * FROM device WHERE bridge_id=?", value)
case models.RKTag:
err = r.DBX.SelectContext(ctx, &records, "SELECT device.* FROM device JOIN device_tag dt ON device.id = dt.device_id WHERE dt.tag_name=?", value)
case models.RKAll:
err = r.DBX.SelectContext(ctx, &records, "SELECT device.* FROM device")
default:
log.Println("Unknown reference kind used for device fetch:", kind)
return []models.Device{}, nil
}
if err != nil {
return nil, dbErr(err)
}
return r.populate(ctx, records)
}
func (r *DeviceRepo) Save(ctx context.Context, device *models.Device) error {
tx, err := r.DBX.Beginx()
if err != nil {
return dbErr(err)
}
defer tx.Rollback()
record := deviceRecord{
ID: device.ID,
BridgeID: device.BridgeID,
InternalID: device.InternalID,
Icon: device.Icon,
Name: device.Name,
Capabilities: strings.Join(models.DeviceCapabilitiesToStrings(device.Capabilities), ","),
ButtonNames: strings.Join(device.ButtonNames, ","),
}
if device.ID > 0 {
_, err := tx.NamedExecContext(ctx, `
UPDATE device SET
internal_id = :internal_id,
icon = :icon,
name = :name,
capabilities = :capabilities,
button_names = :button_names
WHERE id=:id
`, record)
if err != nil {
return dbErr(err)
}
// Let's just be lazy for now, optimize later if need be.
_, err = tx.ExecContext(ctx, "DELETE FROM device_tag WHERE device_id=?", record.ID)
if err != nil {
return dbErr(err)
}
_, err = tx.ExecContext(ctx, "DELETE FROM device_property WHERE device_id=?", record.ID)
if err != nil {
return dbErr(err)
}
} else {
res, err := tx.NamedExecContext(ctx, `
INSERT INTO device (bridge_id, internal_id, icon, name, capabilities, button_names)
VALUES (:bridge_id, :internal_id, :icon, :name, :capabilities, :button_names)
`, record)
if err != nil {
return dbErr(err)
}
lastID, err := res.LastInsertId()
if err != nil {
return dbErr(err)
}
record.ID = int(lastID)
device.ID = int(lastID)
}
for _, tag := range device.Tags {
_, err := tx.ExecContext(ctx, "INSERT INTO device_tag (device_id, tag_name) VALUES (?, ?)", record.ID, tag)
if err != nil {
return dbErr(err)
}
}
for key, value := range device.UserProperties {
_, err := tx.ExecContext(ctx, "INSERT INTO device_property (device_id, prop_key, prop_value, is_user) VALUES (?, ?, ?, 1)",
record.ID, key, value,
)
if err != nil {
return dbErr(err)
}
}
for key, value := range device.DriverProperties {
j, err := json.Marshal(value)
if err != nil {
// Eh, it'll get filled by the driver anyway
continue
}
_, err = tx.ExecContext(ctx, "INSERT INTO device_property (device_id, prop_key, prop_value, is_user) VALUES (?, ?, ?, 0)",
record.ID, key, string(j),
)
if err != nil {
// Return err here anyway, it might put the tx in a bad state to ignore it.
return dbErr(err)
}
}
_, err = tx.NamedExecContext(ctx, `
REPLACE INTO device_state(device_id, hue, saturation, kelvin, power, intensity)
VALUES (:device_id, :hue, :saturation, :kelvin, :power, :intensity)
`, deviceStateRecord{
DeviceID: record.ID,
Hue: device.State.Color.Hue,
Saturation: device.State.Color.Saturation,
Kelvin: device.State.Color.Kelvin,
Power: device.State.Power,
Intensity: device.State.Intensity,
})
if err != nil {
return dbErr(err)
}
return tx.Commit()
}
func (r *DeviceRepo) Delete(ctx context.Context, device *models.Device) error {
_, err := r.DBX.ExecContext(ctx, "DELETE FROM device WHERE Id=?", device.ID)
if err != nil {
return dbErr(err)
}
return nil
}
func (r *DeviceRepo) populateOne(ctx context.Context, record deviceRecord) (*models.Device, error) {
records, err := r.populate(ctx, []deviceRecord{record})
if err != nil {
return nil, err
}
return &records[0], nil
}
func (r *DeviceRepo) populate(ctx context.Context, records []deviceRecord) ([]models.Device, error) {
if len(records) == 0 {
return []models.Device{}, nil
}
ids := make([]int, 0, len(records))
for _, record := range records {
ids = append(ids, record.ID)
}
tagsQuery, tagsArgs, err := sq.Select("*").From("device_tag").Where(sq.Eq{"device_id": ids}).ToSql()
if err != nil {
return nil, dbErr(err)
}
propsQuery, propsArgs, err := sq.Select("*").From("device_property").Where(sq.Eq{"device_id": ids}).ToSql()
if err != nil {
return nil, dbErr(err)
}
stateQuery, stateArgs, err := sq.Select("*").From("device_state").Where(sq.Eq{"device_id": ids}).ToSql()
if err != nil {
return nil, dbErr(err)
}
states := make([]deviceStateRecord, 0, len(records))
props := make([]devicePropertyRecord, 0, len(records)*8)
tags := make([]deviceTagRecord, 0, len(records)*4)
err = r.DBX.SelectContext(ctx, &states, stateQuery, stateArgs...)
if err != nil {
return nil, dbErr(err)
}
err = r.DBX.SelectContext(ctx, &props, propsQuery, propsArgs...)
if err != nil {
return nil, dbErr(err)
}
err = r.DBX.SelectContext(ctx, &tags, tagsQuery, tagsArgs...)
if err != nil {
return nil, dbErr(err)
}
devices := make([]models.Device, 0, len(records))
for _, record := range records {
device := models.Device{
ID: record.ID,
BridgeID: record.BridgeID,
InternalID: record.InternalID,
Icon: record.Icon,
Name: record.Name,
ButtonNames: strings.Split(record.ButtonNames, ","),
DriverProperties: make(map[string]interface{}, 8),
UserProperties: make(map[string]string, 8),
Tags: make([]string, 0, 8),
}
caps := make([]models.DeviceCapability, 0, 16)
for _, capStr := range strings.Split(record.Capabilities, ",") {
caps = append(caps, models.DeviceCapability(capStr))
}
device.Capabilities = caps
for _, state := range states {
if state.DeviceID == record.ID {
device.State = models.DeviceState{
Power: state.Power,
Color: models.ColorValue{
Hue: state.Hue,
Saturation: state.Saturation,
Kelvin: state.Kelvin,
},
Intensity: state.Intensity,
}
}
}
driverProps := make(map[string]json.RawMessage, 8)
for _, prop := range props {
if prop.DeviceID == record.ID {
if prop.IsUser {
device.UserProperties[prop.Key] = prop.Value
} else {
driverProps[prop.Key] = json.RawMessage(prop.Value)
}
}
}
if len(driverProps) > 0 {
j, err := json.Marshal(driverProps)
if err != nil {
return nil, dbErr(err)
}
err = json.Unmarshal(j, &device.DriverProperties)
if err != nil {
return nil, dbErr(err)
}
}
for _, tag := range tags {
if tag.DeviceID == record.ID {
device.Tags = append(device.Tags, tag.TagName)
}
}
devices = append(devices, device)
}
return devices, nil
}

17
models/device.go

@ -34,19 +34,28 @@ type DeviceState struct {
type NewDeviceState struct {
Power *bool `json:"power"`
Color *string `json:"color"`
Intensity int `json:"intensity"`
Intensity *float64 `json:"intensity"`
Temperature int `json:"temperature"`
}
type DeviceCapability string
type DeviceRepository interface {
FindByID(ctx context.Context, id int) (*Device, error)
Find(ctx context.Context, id int) (*Device, error)
FetchByReference(ctx context.Context, kind ReferenceKind, value string) ([]Device, error)
Save(ctx context.Context, device *Device) error
Delete(ctx context.Context, device *Device) error
}
func DeviceCapabilitiesToStrings(caps []DeviceCapability) []string {
res := make([]string, 0, len(caps))
for _, cap := range caps {
res = append(res, string(cap))
}
return res
}
var (
DCPower DeviceCapability = "Power"
DCColorHS DeviceCapability = "ColorHS"
@ -118,5 +127,9 @@ func (d *Device) SetState(newState NewDeviceState) error {
}
}
if newState.Intensity != nil && d.HasCapability(DCIntensity) {
d.State.Intensity = *newState.Intensity
}
return nil
}

1
models/shared.go

@ -6,6 +6,7 @@ var (
RKDeviceID ReferenceKind = "DeviceID"
RKBridgeID ReferenceKind = "BridgeID"
RKTag ReferenceKind = "Tag"
RKAll ReferenceKind = "All"
)

5
scripts/20210522140146_device.sql

@ -5,12 +5,13 @@ CREATE TABLE device
id INT NOT NULL AUTO_INCREMENT,
bridge_id INT NOT NULL,
internal_id VARCHAR(255) NOT NULL,
icon CHAR NOT NULL,
icon VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
capabilities VARCHAR(255) NOT NULL,
button_names VARCHAR(255) NOT NULL,
PRIMARY KEY (id)
PRIMARY KEY (id),
UNIQUE (bridge_id, internal_id)
);
-- +goose StatementEnd

2
scripts/20210522140148_device_state.sql

@ -7,7 +7,7 @@ CREATE TABLE device_state
saturation DOUBLE NOT NULL,
kelvin INT NOT NULL,
power TINYINT NOT NULL,
intensity INT NOT NULL,
intensity DOUBLE NOT NULL,
PRIMARY KEY (device_id)
);

16
scripts/20210918105052_device_tag.sql

@ -0,0 +1,16 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE device_tag
(
device_id INT NOT NULL,
tag_name VARCHAR(255) NOT NULL,
PRIMARY KEY (device_id, tag_name),
INDEX (tag_name)
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE device_tag;
-- +goose StatementEnd
Loading…
Cancel
Save