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 package api
import ( import (
"fmt"
"git.aiterp.net/lucifer/new-server/app/config" "git.aiterp.net/lucifer/new-server/app/config"
"git.aiterp.net/lucifer/new-server/models" "git.aiterp.net/lucifer/new-server/models"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -18,9 +19,11 @@ func Bridges(r gin.IRoutes) {
r.POST("", handler(func(c *gin.Context) (interface{}, error) { r.POST("", handler(func(c *gin.Context) (interface{}, error) {
var body struct { 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) err := parseBody(c, &body)
if err != nil { if err != nil {
@ -32,6 +35,22 @@ func Bridges(r gin.IRoutes) {
return nil, err 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) bridges, err := driver.SearchBridge(ctxOf(c), body.Address, body.DryRun)
if err != nil { if err != nil {
return nil, err 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 dp models.DriverProvider
var dpMutex sync.Mutex
var dpOnce sync.Once
func DriverProvider() models.DriverProvider { func DriverProvider() models.DriverProvider {
dpMutex.Lock()
defer dpMutex.Unlock()
if dp == nil {
dpOnce.Do(func() {
dp = drivers.DriverMap{ dp = drivers.DriverMap{
models.DTNanoLeaf: &nanoleaf.Driver{}, models.DTNanoLeaf: &nanoleaf.Driver{},
models.DTHue: &hue.Driver{}, models.DTHue: &hue.Driver{},
models.DTLIFX: &lifx.Driver{}, models.DTLIFX: &lifx.Driver{},
} }
}
})
return dp return dp
} }

2
app/config/repo.go

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

4
app/server.go

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

34
app/services/bridges.go

@ -3,11 +3,15 @@ package services
import ( import (
"context" "context"
"git.aiterp.net/lucifer/new-server/app/config" "git.aiterp.net/lucifer/new-server/app/config"
"git.aiterp.net/lucifer/new-server/models"
"log" "log"
"strconv"
"sync"
"time" "time"
) )
var cancelMap = make(map[int]context.CancelFunc, 8) var cancelMap = make(map[int]context.CancelFunc, 8)
var cancelMutex sync.Mutex
func ConnectToBridges() { func ConnectToBridges() {
go func() { go func() {
@ -29,7 +33,10 @@ func runConnectToBridges() error {
} }
for _, bridge := range bridges { for _, bridge := range bridges {
if cancelMap[bridge.ID] != nil {
cancelMutex.Lock()
isRunning := cancelMap[bridge.ID] != nil
cancelMutex.Unlock()
if isRunning {
continue continue
} }
@ -39,11 +46,30 @@ func runConnectToBridges() error {
} }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
err = driver.Run(ctx, bridge, config.EventChannel)
cancelMutex.Lock()
cancelMap[bridge.ID] = cancel 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 return nil

12
app/services/events.go

@ -2,10 +2,12 @@ package services
import ( import (
"context" "context"
"fmt"
"git.aiterp.net/lucifer/new-server/app/config" "git.aiterp.net/lucifer/new-server/app/config"
"git.aiterp.net/lucifer/new-server/models" "git.aiterp.net/lucifer/new-server/models"
"log" "log"
"strconv" "strconv"
"strings"
"time" "time"
) )
@ -18,7 +20,7 @@ func StartEventHandler() {
} }
}() }()
// Dispatch an HourChanged event at every hour
// Generate TimeChanged event
go func() { go func() {
drift := time.Now().Add(time.Minute).Truncate(time.Minute).Sub(time.Now()) drift := time.Now().Add(time.Minute).Truncate(time.Minute).Sub(time.Now())
time.Sleep(drift + time.Millisecond * 5) time.Sleep(drift + time.Millisecond * 5)
@ -48,7 +50,12 @@ func handleEvent(event models.Event) {
} }
if !X { 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 return
} }
@ -68,7 +75,6 @@ func handleEvent(event models.Event) {
if !handler.MatchesEvent(event, devices) { if !handler.MatchesEvent(event, devices) {
continue 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/app/config"
"git.aiterp.net/lucifer/new-server/models" "git.aiterp.net/lucifer/new-server/models"
"log" "log"
"sync"
"time"
) )
func StartPublisher() { func StartPublisher() {
@ -16,32 +18,58 @@ func StartPublisher() {
continue 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 { 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 { if err != nil {
log.Println("Publishing error (1): " + err.Error()) log.Println("Publishing error (1): " + err.Error())
continue 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" "git.aiterp.net/lucifer/new-server/app/config"
"github.com/pressly/goose" "github.com/pressly/goose"
"log" "log"
"os"
"time" "time"
) )
@ -19,7 +20,12 @@ func main() {
log.Fatal(err) 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 { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

7
go.mod

@ -3,14 +3,15 @@ module git.aiterp.net/lucifer/new-server
go 1.16 go 1.16
require ( require (
github.com/Masterminds/squirrel v1.5.0
github.com/gin-gonic/gin v1.7.1 github.com/gin-gonic/gin v1.7.1
github.com/go-sql-driver/mysql v1.6.0 github.com/go-sql-driver/mysql v1.6.0
github.com/jmoiron/sqlx v1.3.4 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/lucasb-eyer/go-colorful v1.2.0
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pressly/goose v2.7.0+incompatible 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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/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 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 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 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 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.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 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 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 h1:PWejVEv07LCerQEzMMeAtjuyCKbyprZ/LBa6K5P0OCQ=
github.com/pressly/goose v2.7.0+incompatible/go.mod h1:m+QHWCqxR3k8D9l7qfzuC/djtlfzxr34mozWDYEu1z8= 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/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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 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= 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-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-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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 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= 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-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 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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/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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 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) { 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) 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 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) { func (b *BridgeRepo) FetchAll(ctx context.Context) ([]models.Bridge, error) {
bridges := make([]models.Bridge, 0, 8) 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 { if err != nil {
return nil, dbErr(err) 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 { type NewDeviceState struct {
Power *bool `json:"power"` Power *bool `json:"power"`
Color *string `json:"color"` Color *string `json:"color"`
Intensity int `json:"intensity"`
Intensity *float64 `json:"intensity"`
Temperature int `json:"temperature"` Temperature int `json:"temperature"`
} }
type DeviceCapability string type DeviceCapability string
type DeviceRepository interface { 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) FetchByReference(ctx context.Context, kind ReferenceKind, value string) ([]Device, error)
Save(ctx context.Context, device *Device) error Save(ctx context.Context, device *Device) error
Delete(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 ( var (
DCPower DeviceCapability = "Power" DCPower DeviceCapability = "Power"
DCColorHS DeviceCapability = "ColorHS" 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 return nil
} }

1
models/shared.go

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

5
scripts/20210522140146_device.sql

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

2
scripts/20210522140148_device_state.sql

@ -7,7 +7,7 @@ CREATE TABLE device_state
saturation DOUBLE NOT NULL, saturation DOUBLE NOT NULL,
kelvin INT NOT NULL, kelvin INT NOT NULL,
power TINYINT NOT NULL, power TINYINT NOT NULL,
intensity INT NOT NULL,
intensity DOUBLE NOT NULL,
PRIMARY KEY (device_id) 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