small fixed, device repo and some device api
This commit is contained in:
+22
-3
@@ -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"`
|
Driver models.DriverKind `json:"driver"`
|
||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
DryRun bool `json:"dryRun"`
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -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()
|
dpOnce.Do(func() {
|
||||||
defer dpMutex.Unlock()
|
|
||||||
|
|
||||||
if dp == nil {
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -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
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
+30
-4
@@ -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("Connected to bridge \"%s\" (%d)", bridge.Name, bridge.ID)
|
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)
|
||||||
|
|
||||||
|
cancelMutex.Lock()
|
||||||
|
cancel()
|
||||||
|
cancelMap[bridge.ID] = nil
|
||||||
|
cancelMutex.Unlock()
|
||||||
|
}(bridge, cancel)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+44
-16
@@ -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!
|
lists := make(map[int][]models.Device, 4)
|
||||||
// Send devices not belonging to the first channel separately
|
|
||||||
bridgeID := devices[0].BridgeID
|
|
||||||
for _, device := range devices {
|
for _, device := range devices {
|
||||||
if device.BridgeID != bridgeID {
|
lists[device.BridgeID] = append(lists[device.BridgeID], device)
|
||||||
config.PublishChannel<-[]models.Device{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)
|
wg := sync.WaitGroup{}
|
||||||
if err != nil {
|
for _, devices := range lists {
|
||||||
log.Println("Publishing error (2): " + err.Error())
|
wg.Add(1)
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = driver.Publish(ctx, bridge, devices)
|
wg.Wait()
|
||||||
if err != nil {
|
cancel()
|
||||||
log.Println("Publishing error (3): " + err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
+7
-1
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/crypto v0.0.0-20210915214749-c084706c2272 // indirect
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg=
|
||||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
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-20210915214749-c084706c2272 h1:3erb+vDS8lU1sxfDHF4/hhWyaXnhIaO+7RgL4fDZORA=
|
||||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
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=
|
||||||
|
|||||||
@@ -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 {
|
if err != nil {
|
||||||
return models.Bridge{}, dbErr(err)
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
+15
-2
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user