This commit is contained in:
+58
-2
@@ -3,11 +3,14 @@ package api
|
|||||||
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/app/services/scene"
|
||||||
"git.aiterp.net/lucifer/new-server/models"
|
"git.aiterp.net/lucifer/new-server/models"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
"log"
|
"log"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func fetchDevices(ctx context.Context, fetchStr string) ([]models.Device, error) {
|
func fetchDevices(ctx context.Context, fetchStr string) ([]models.Device, error) {
|
||||||
@@ -71,12 +74,15 @@ func Devices(r gin.IRoutes) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = scene.GlobalManager().UpdateDevice(ctxOf(c), &devices[i], nil)
|
||||||
|
|
||||||
set[devices[i].ID] = true
|
set[devices[i].ID] = true
|
||||||
changed = append(changed, devices[i])
|
changed = append(changed, devices[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config.PublishChannel <- changed
|
|
||||||
|
config.PublishChannel <- scene.GlobalManager().FilterUnassigned(changed)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for _, device := range changed {
|
for _, device := range changed {
|
||||||
@@ -139,9 +145,11 @@ func Devices(r gin.IRoutes) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = scene.GlobalManager().UpdateDevice(ctxOf(c), &devices[i], nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
config.PublishChannel <- devices
|
config.PublishChannel <- scene.GlobalManager().FilterUnassigned(devices)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for _, device := range devices {
|
for _, device := range devices {
|
||||||
@@ -212,4 +220,52 @@ func Devices(r gin.IRoutes) {
|
|||||||
|
|
||||||
return devices, nil
|
return devices, nil
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
r.PUT("/:fetch/scene", handler(func(c *gin.Context) (interface{}, error) {
|
||||||
|
var body models.DeviceSceneAssignment
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
assignedScene, err := config.SceneRepository().Find(ctxOf(c), body.SceneID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if body.DurationMS < 0 {
|
||||||
|
body.DurationMS = 0
|
||||||
|
}
|
||||||
|
body.StartTime = time.Now()
|
||||||
|
|
||||||
|
for i := range devices {
|
||||||
|
devices[i].SceneAssignments = []models.DeviceSceneAssignment{body}
|
||||||
|
|
||||||
|
err := scene.GlobalManager().UpdateDevice(ctxOf(c), &devices[i], assignedScene)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eg := errgroup.Group{}
|
||||||
|
for i := range devices {
|
||||||
|
eg.Go(func() error {
|
||||||
|
return config.DeviceRepository().Save(ctxOf(c), &devices[i], 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
err = eg.Wait()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return devices, nil
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.aiterp.net/lucifer/new-server/app/config"
|
||||||
|
"git.aiterp.net/lucifer/new-server/models"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Scenes(r gin.IRoutes) {
|
||||||
|
r.GET("", handler(func(c *gin.Context) (interface{}, error) {
|
||||||
|
return config.SceneRepository().FetchAll(ctxOf(c))
|
||||||
|
}))
|
||||||
|
|
||||||
|
r.GET("/:id", handler(func(c *gin.Context) (interface{}, error) {
|
||||||
|
return config.SceneRepository().Find(ctxOf(c), intParam(c, "id"))
|
||||||
|
}))
|
||||||
|
|
||||||
|
r.POST("", handler(func(c *gin.Context) (interface{}, error) {
|
||||||
|
var body models.Scene
|
||||||
|
err := parseBody(c, &body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = body.Validate()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = config.SceneRepository().Save(ctxOf(c), &body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return body, nil
|
||||||
|
}))
|
||||||
|
}
|
||||||
+3
-2
@@ -24,8 +24,9 @@ func DBX() *sqlx.DB {
|
|||||||
MySqlSchema(),
|
MySqlSchema(),
|
||||||
))
|
))
|
||||||
|
|
||||||
dbx.SetMaxIdleConns(50)
|
dbx.SetMaxIdleConns(20)
|
||||||
dbx.SetMaxOpenConns(100)
|
dbx.SetMaxOpenConns(40)
|
||||||
|
dbx.SetConnMaxLifetime(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
return dbx
|
return dbx
|
||||||
|
|||||||
@@ -20,3 +20,7 @@ func DeviceRepository() models.DeviceRepository {
|
|||||||
func EventHandlerRepository() models.EventHandlerRepository {
|
func EventHandlerRepository() models.EventHandlerRepository {
|
||||||
return &mysql.EventHandlerRepo{DBX: DBX()}
|
return &mysql.EventHandlerRepo{DBX: DBX()}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SceneRepository() models.SceneRepository {
|
||||||
|
return &mysql.SceneRepo{DBX: DBX()}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.aiterp.net/lucifer/new-server/app/api"
|
"git.aiterp.net/lucifer/new-server/app/api"
|
||||||
"git.aiterp.net/lucifer/new-server/app/config"
|
"git.aiterp.net/lucifer/new-server/app/config"
|
||||||
"git.aiterp.net/lucifer/new-server/app/services"
|
"git.aiterp.net/lucifer/new-server/app/services"
|
||||||
|
"git.aiterp.net/lucifer/new-server/app/services/scene"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"log"
|
"log"
|
||||||
)
|
)
|
||||||
@@ -15,6 +17,8 @@ func StartServer() {
|
|||||||
services.ConnectToBridges()
|
services.ConnectToBridges()
|
||||||
services.CheckNewDevices()
|
services.CheckNewDevices()
|
||||||
|
|
||||||
|
go scene.GlobalManager().Run(context.Background())
|
||||||
|
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
ginny := gin.New()
|
ginny := gin.New()
|
||||||
|
|
||||||
@@ -25,6 +29,7 @@ func StartServer() {
|
|||||||
api.DriverKinds(apiGin.Group("/driver-kinds"))
|
api.DriverKinds(apiGin.Group("/driver-kinds"))
|
||||||
api.Events(apiGin.Group("/events"))
|
api.Events(apiGin.Group("/events"))
|
||||||
api.EventHandlers(apiGin.Group("/event-handlers"))
|
api.EventHandlers(apiGin.Group("/event-handlers"))
|
||||||
|
api.Scenes(apiGin.Group("/scenes"))
|
||||||
|
|
||||||
log.Fatal(ginny.Run(fmt.Sprintf("0.0.0.0:%d", config.ServerPort())))
|
log.Fatal(ginny.Run(fmt.Sprintf("0.0.0.0:%d", config.ServerPort())))
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-7
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.aiterp.net/lucifer/new-server/app/config"
|
"git.aiterp.net/lucifer/new-server/app/config"
|
||||||
|
"git.aiterp.net/lucifer/new-server/app/services/scene"
|
||||||
"git.aiterp.net/lucifer/new-server/models"
|
"git.aiterp.net/lucifer/new-server/models"
|
||||||
"log"
|
"log"
|
||||||
"math"
|
"math"
|
||||||
@@ -150,21 +151,31 @@ func handleEvent(event models.Event) (responses []models.Event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config.PublishChannel <- allDevices
|
config.PublishChannel <- scene.GlobalManager().FilterUnassigned(allDevices)
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
for _, device := range allDevices {
|
|
||||||
wg.Add(1)
|
|
||||||
|
|
||||||
go func(device models.Device) {
|
if len(allDevices) > 0 {
|
||||||
err := config.DeviceRepository().Save(context.Background(), &device, models.SMState)
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
err := config.DeviceRepository().SaveMany(context.Background(), models.SMState, allDevices)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Failed to save device for state:", err)
|
log.Println("Failed to save devices' state:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}(device)
|
}()
|
||||||
|
|
||||||
|
for _, device := range allDevices {
|
||||||
|
go func(device models.Device) {
|
||||||
|
err := scene.GlobalManager().UpdateDevice(context.Background(), &device, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to update devices' scene in resposne to event:", err)
|
||||||
|
}
|
||||||
|
}(device)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, handler := range deadHandlers {
|
for _, handler := range deadHandlers {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ func StartPublisher() {
|
|||||||
}
|
}
|
||||||
if bridge.ID == 0 {
|
if bridge.ID == 0 {
|
||||||
log.Println("Unknown bridge")
|
log.Println("Unknown bridge")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if bridge.Driver == models.DTLIFX {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
bridge, err := config.BridgeRepository().Find(ctx, devices[0].BridgeID)
|
bridge, err := config.BridgeRepository().Find(ctx, devices[0].BridgeID)
|
||||||
|
|||||||
@@ -1,27 +1,84 @@
|
|||||||
package scene
|
package scene
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"git.aiterp.net/lucifer/new-server/app/config"
|
||||||
"git.aiterp.net/lucifer/new-server/models"
|
"git.aiterp.net/lucifer/new-server/models"
|
||||||
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
assignments map[int]*Scene
|
allocations map[int]*Scene
|
||||||
|
devices map[int]*models.Device
|
||||||
scenes []*Scene
|
scenes []*Scene
|
||||||
|
|
||||||
|
notifyCh chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mgr *Manager) UpdateDevice(device *models.Device) {
|
func (mgr *Manager) UpdateDevice(ctx context.Context, device *models.Device, data *models.Scene) error {
|
||||||
|
mgr.mu.Lock()
|
||||||
|
defer mgr.mu.Unlock()
|
||||||
|
|
||||||
|
scene, err := mgr.findScene(ctx, device.SceneAssignments, data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if mgr.allocations[device.ID] != nil && mgr.allocations[device.ID] != scene {
|
||||||
|
mgr.allocations[device.ID].RemoveDevice(*device)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mgr.allocations[device.ID] != scene {
|
||||||
|
if mgr.allocations == nil {
|
||||||
|
mgr.allocations = make(map[int]*Scene, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr.allocations[device.ID] = scene
|
||||||
|
|
||||||
|
if scene != nil {
|
||||||
|
scene.due = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if scene != nil {
|
||||||
|
scene.UpsertDevice(*device)
|
||||||
|
|
||||||
|
if mgr.devices == nil {
|
||||||
|
mgr.devices = make(map[int]*models.Device)
|
||||||
|
}
|
||||||
|
mgr.devices[device.ID] = device
|
||||||
|
} else {
|
||||||
|
config.PublishChannel <- []models.Device{*device}
|
||||||
|
}
|
||||||
|
|
||||||
|
if mgr.notifyCh != nil {
|
||||||
|
close(mgr.notifyCh)
|
||||||
|
mgr.notifyCh = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mgr *Manager) GetUnassigned(devices []models.Device) []models.Device {
|
func (mgr *Manager) UpdateScene(data *models.Scene) {
|
||||||
|
mgr.mu.Lock()
|
||||||
|
for _, scene := range mgr.scenes {
|
||||||
|
if scene.data.ID == data.ID {
|
||||||
|
scene.UpdateScene(scene.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mgr.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mgr *Manager) FilterUnassigned(devices []models.Device) []models.Device {
|
||||||
mgr.mu.Lock()
|
mgr.mu.Lock()
|
||||||
defer mgr.mu.Unlock()
|
defer mgr.mu.Unlock()
|
||||||
|
|
||||||
res := make([]models.Device, 0, len(devices))
|
res := make([]models.Device, 0, len(devices))
|
||||||
for _, device := range devices {
|
for _, device := range devices {
|
||||||
if _, ok := mgr.assignments[device.ID]; !ok {
|
if mgr.allocations[device.ID] == nil {
|
||||||
res = append(res, device)
|
res = append(res, device)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,8 +86,153 @@ func (mgr *Manager) GetUnassigned(devices []models.Device) []models.Device {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mgr *Manager) Run(ctx context.Context) {
|
||||||
|
devices, err := config.DeviceRepository().FetchByReference(ctx, models.RKAll, "")
|
||||||
|
if err == nil {
|
||||||
|
for _, device := range devices {
|
||||||
|
_ = mgr.UpdateDevice(ctx, &device, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ticker := time.NewTicker(time.Millisecond * 101)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
mgr.mu.Lock()
|
||||||
|
mgr.notifyCh = make(chan struct{})
|
||||||
|
mgr.mu.Unlock()
|
||||||
|
|
||||||
|
for {
|
||||||
|
mgr.mu.Lock()
|
||||||
|
if mgr.notifyCh == nil {
|
||||||
|
mgr.notifyCh = make(chan struct{})
|
||||||
|
}
|
||||||
|
notifyCh := mgr.notifyCh
|
||||||
|
mgr.mu.Unlock()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
case <-notifyCh:
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout, cancel := context.WithTimeout(ctx, time.Millisecond*100)
|
||||||
|
|
||||||
|
var updates []models.Device
|
||||||
|
mgr.mu.Lock()
|
||||||
|
for _, device := range mgr.devices {
|
||||||
|
if device == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if mgr.reScene(timeout, device) {
|
||||||
|
updates = append(updates, *device)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, scene := range mgr.scenes {
|
||||||
|
if scene.Due() {
|
||||||
|
scene.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
updates = append(updates, scene.Flush()...)
|
||||||
|
}
|
||||||
|
mgr.mu.Unlock()
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
if len(updates) > 0 {
|
||||||
|
log.Println("Updating", len(updates), "devices")
|
||||||
|
config.PublishChannel <- updates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mgr *Manager) reScene(ctx context.Context, device *models.Device) bool {
|
||||||
|
scene, err := mgr.findScene(ctx, device.SceneAssignments, nil)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if mgr.allocations[device.ID] != nil && mgr.allocations[device.ID] != scene {
|
||||||
|
mgr.allocations[device.ID].RemoveDevice(*device)
|
||||||
|
}
|
||||||
|
if mgr.allocations[device.ID] != scene {
|
||||||
|
if mgr.allocations == nil {
|
||||||
|
mgr.allocations = make(map[int]*Scene, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr.allocations[device.ID] = scene
|
||||||
|
|
||||||
|
if scene == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr.allocations[device.ID].UpsertDevice(*device)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mgr *Manager) findScene(ctx context.Context, assignments []models.DeviceSceneAssignment, data *models.Scene) (*Scene, error) {
|
||||||
|
if len(assignments) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var selected *models.DeviceSceneAssignment
|
||||||
|
for _, assignment := range assignments {
|
||||||
|
pos := int64(time.Since(assignment.StartTime) / time.Millisecond)
|
||||||
|
if assignment.DurationMS == 0 || (pos >= -1 && pos < assignment.DurationMS) {
|
||||||
|
selected = &assignment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if selected == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, scene := range mgr.scenes {
|
||||||
|
if scene.group == selected.Group && scene.startTime.Equal(selected.StartTime) && selected.DurationMS == scene.duration.Milliseconds() {
|
||||||
|
return scene, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if data == nil {
|
||||||
|
for i := range mgr.scenes {
|
||||||
|
if mgr.scenes[i].data.ID == selected.SceneID {
|
||||||
|
data = &mgr.scenes[i].data
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if data == nil {
|
||||||
|
var err error
|
||||||
|
data, err = config.SceneRepository().Find(ctx, selected.SceneID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scene := &Scene{
|
||||||
|
startTime: selected.StartTime,
|
||||||
|
duration: time.Duration(selected.DurationMS) * time.Millisecond,
|
||||||
|
lastRun: 0,
|
||||||
|
due: true,
|
||||||
|
group: selected.Group,
|
||||||
|
data: *data,
|
||||||
|
roleList: make([][]models.Device, len(data.Roles)),
|
||||||
|
deviceMap: make(map[int]*models.Device, 8),
|
||||||
|
roleMap: make(map[int]int, 8),
|
||||||
|
changes: make(map[int]models.NewDeviceState, 8),
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr.scenes = append(mgr.scenes, scene)
|
||||||
|
|
||||||
|
return scene, nil
|
||||||
|
}
|
||||||
|
|
||||||
var globalManager = &Manager{}
|
var globalManager = &Manager{}
|
||||||
|
|
||||||
func Get() *Manager {
|
func GlobalManager() *Manager {
|
||||||
return globalManager
|
return globalManager
|
||||||
}
|
}
|
||||||
|
|||||||
+113
-25
@@ -1,14 +1,19 @@
|
|||||||
package scene
|
package scene
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"git.aiterp.net/lucifer/new-server/app/config"
|
||||||
"git.aiterp.net/lucifer/new-server/models"
|
"git.aiterp.net/lucifer/new-server/models"
|
||||||
|
"math"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Scene struct {
|
type Scene struct {
|
||||||
startTime time.Time
|
startTime time.Time
|
||||||
duration time.Duration
|
duration time.Duration
|
||||||
tag string
|
lastRun int64
|
||||||
|
due bool
|
||||||
|
group string
|
||||||
data models.Scene
|
data models.Scene
|
||||||
roleList [][]models.Device
|
roleList [][]models.Device
|
||||||
deviceMap map[int]*models.Device
|
deviceMap map[int]*models.Device
|
||||||
@@ -21,32 +26,110 @@ func (s *Scene) UpdateScene(scene models.Scene) {
|
|||||||
s.roleMap = make(map[int]int)
|
s.roleMap = make(map[int]int)
|
||||||
|
|
||||||
for _, device := range s.deviceMap {
|
for _, device := range s.deviceMap {
|
||||||
s.moveDevice(*device)
|
s.moveDevice(*device, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i, role := range s.data.Roles {
|
||||||
|
role.ApplyOrder(s.roleList[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Scene) UpsertDevice(device models.Device) {
|
func (s *Scene) UpsertDevice(device models.Device) {
|
||||||
s.deviceMap[device.ID] = &device
|
s.deviceMap[device.ID] = &device
|
||||||
s.moveDevice(device)
|
s.moveDevice(device, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Scene) RemoveDevice(device models.Device) {
|
func (s *Scene) RemoveDevice(device models.Device) {
|
||||||
|
roleIndex, hasRole := s.roleMap[device.ID]
|
||||||
|
|
||||||
delete(s.deviceMap, device.ID)
|
delete(s.deviceMap, device.ID)
|
||||||
delete(s.roleMap, device.ID)
|
delete(s.roleMap, device.ID)
|
||||||
|
|
||||||
|
if hasRole {
|
||||||
|
s.runRole(roleIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scene) Due() bool {
|
||||||
|
return s.due || s.intervalNumber() != s.lastRun
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scene) Flush() []models.Device {
|
||||||
|
res := make([]models.Device, 0, 8)
|
||||||
|
for key, change := range s.changes {
|
||||||
|
if s.deviceMap[key] != nil {
|
||||||
|
device := *s.deviceMap[key]
|
||||||
|
err := device.SetState(change)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
res = append(res, device)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(s.changes, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scene) Run() {
|
||||||
|
s.lastRun = s.intervalNumber()
|
||||||
|
s.due = false
|
||||||
|
|
||||||
|
for index := range s.roleList {
|
||||||
|
s.runRole(index)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Scene) runRole(index int) {
|
func (s *Scene) runRole(index int) {
|
||||||
|
role := s.data.Roles[index]
|
||||||
|
devices := s.roleList[index]
|
||||||
|
|
||||||
|
intervalNumber := s.intervalNumber()
|
||||||
|
intervalMax := s.intervalMax()
|
||||||
|
|
||||||
|
for i, device := range devices {
|
||||||
|
newState, changed := role.ApplyEffect(&device, models.SceneRunContext{
|
||||||
|
Index: i,
|
||||||
|
Length: len(devices),
|
||||||
|
IntervalNumber: intervalNumber,
|
||||||
|
IntervalMax: intervalMax,
|
||||||
|
})
|
||||||
|
|
||||||
|
if !changed {
|
||||||
|
go func() {
|
||||||
|
_ = config.DeviceRepository().Save(context.Background(), &device, models.SMState)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
s.changes[device.ID] = newState
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Scene) moveDevice(device models.Device, run bool) {
|
func (s *Scene) moveDevice(device models.Device, run bool) {
|
||||||
oldRole, hasOldRole := s.roleMap[device.ID]
|
oldRole, hasOldRole := s.roleMap[device.ID]
|
||||||
newRole := s.data.RoleIndex(&device)
|
newRole := s.data.RoleIndex(&device)
|
||||||
|
|
||||||
|
// If it doesn't move between roles, just update and reorder it.
|
||||||
if hasOldRole && oldRole == newRole {
|
if hasOldRole && oldRole == newRole {
|
||||||
|
for i, device2 := range s.roleList[newRole] {
|
||||||
|
if device2.ID == device.ID {
|
||||||
|
s.roleList[newRole][i] = device
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if run {
|
||||||
|
s.data.Roles[newRole].ApplyOrder(s.roleList[newRole])
|
||||||
|
s.runRole(newRole)
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Take it out of the old role.
|
||||||
if hasOldRole {
|
if hasOldRole {
|
||||||
for i, device2 := range s.roleList[oldRole] {
|
for i, device2 := range s.roleList[oldRole] {
|
||||||
if device2.ID == device.ID {
|
if device2.ID == device.ID {
|
||||||
@@ -55,33 +138,38 @@ func (s *Scene) moveDevice(device models.Device, run bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s.runRole(oldRole)
|
if run {
|
||||||
|
s.runRole(oldRole)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s.roleMap[device.ID] = newRole
|
if newRole != -1 {
|
||||||
s.runRole(newRole)
|
s.roleMap[device.ID] = newRole
|
||||||
|
s.roleList[newRole] = append(s.roleList[newRole], device)
|
||||||
|
|
||||||
|
if run {
|
||||||
|
s.data.Roles[newRole].ApplyOrder(s.roleList[newRole])
|
||||||
|
s.runRole(newRole)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.due = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Scene) intervalNumber() int64 {
|
func (s *Scene) intervalNumber() int64 {
|
||||||
intervalDuration := time.Duration(s.data.IntervalMS) * time.Millisecond
|
intervalDuration := time.Duration(s.data.IntervalMS) * time.Millisecond
|
||||||
|
if intervalDuration == 0 {
|
||||||
return int64(time.Since(s.startTime) / intervalDuration)
|
return 0
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Scene) intervalFac() float64 {
|
|
||||||
return clamp(float64(time.Since(s.startTime)) / float64(s.duration))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Scene) positionFac(i, len int) float64 {
|
|
||||||
return clamp(float64(i) / float64(len))
|
|
||||||
}
|
|
||||||
|
|
||||||
func clamp(fac float64) float64 {
|
|
||||||
if fac > 1.00 {
|
|
||||||
return 1.00
|
|
||||||
} else if fac < 0.00 {
|
|
||||||
return 0.00
|
|
||||||
} else {
|
|
||||||
return fac
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return int64(math.Round(float64(time.Since(s.startTime)) / float64(intervalDuration)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scene) intervalMax() int64 {
|
||||||
|
intervalDuration := time.Duration(s.data.IntervalMS) * time.Millisecond
|
||||||
|
if intervalDuration == 0 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return int64(s.duration / intervalDuration)
|
||||||
}
|
}
|
||||||
+115
-91
@@ -12,13 +12,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type deviceRecord struct {
|
type deviceRecord struct {
|
||||||
ID int `db:"id"`
|
ID int `db:"id"`
|
||||||
BridgeID int `db:"bridge_id"`
|
BridgeID int `db:"bridge_id"`
|
||||||
InternalID string `db:"internal_id"`
|
InternalID string `db:"internal_id"`
|
||||||
Icon string `db:"icon"`
|
Icon string `db:"icon"`
|
||||||
Name string `db:"name"`
|
Name string `db:"name"`
|
||||||
Capabilities string `db:"capabilities"`
|
Capabilities string `db:"capabilities"`
|
||||||
ButtonNames string `db:"button_names"`
|
ButtonNames string `db:"button_names"`
|
||||||
|
SceneAssignmentJSON json.RawMessage `db:"scene_assignments"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type deviceStateRecord struct {
|
type deviceStateRecord struct {
|
||||||
@@ -117,25 +118,32 @@ func (r *DeviceRepo) FetchByReference(ctx context.Context, kind models.Reference
|
|||||||
return r.populate(ctx, records)
|
return r.populate(ctx, records)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *DeviceRepo) Save(ctx context.Context, device *models.Device, mode models.SaveMode) error {
|
func (r *DeviceRepo) SaveMany(ctx context.Context, mode models.SaveMode, devices []models.Device) error {
|
||||||
tx, err := r.DBX.Beginx()
|
tx, err := r.DBX.Beginx()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return dbErr(err)
|
return dbErr(err)
|
||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
record := deviceRecord{
|
for i, device := range devices {
|
||||||
ID: device.ID,
|
scenesJSON, err := json.Marshal(device.SceneAssignments)
|
||||||
BridgeID: device.BridgeID,
|
if err != nil {
|
||||||
InternalID: device.InternalID,
|
return err
|
||||||
Icon: device.Icon,
|
}
|
||||||
Name: device.Name,
|
|
||||||
Capabilities: strings.Join(models.DeviceCapabilitiesToStrings(device.Capabilities), ","),
|
|
||||||
ButtonNames: strings.Join(device.ButtonNames, ","),
|
|
||||||
}
|
|
||||||
|
|
||||||
if device.ID > 0 {
|
record := deviceRecord{
|
||||||
_, err := tx.NamedExecContext(ctx, `
|
ID: device.ID,
|
||||||
|
BridgeID: device.BridgeID,
|
||||||
|
InternalID: device.InternalID,
|
||||||
|
SceneAssignmentJSON: scenesJSON,
|
||||||
|
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
|
UPDATE device SET
|
||||||
internal_id = :internal_id,
|
internal_id = :internal_id,
|
||||||
icon = :icon,
|
icon = :icon,
|
||||||
@@ -144,98 +152,111 @@ func (r *DeviceRepo) Save(ctx context.Context, device *models.Device, mode model
|
|||||||
button_names = :button_names
|
button_names = :button_names
|
||||||
WHERE id=:id
|
WHERE id=:id
|
||||||
`, record)
|
`, record)
|
||||||
if err != nil {
|
|
||||||
return dbErr(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let's just be lazy for now, optimize later if need be.
|
|
||||||
if mode == 0 || mode&models.SMTags != 0 {
|
|
||||||
_, err = tx.ExecContext(ctx, "DELETE FROM device_tag WHERE device_id=?", record.ID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return dbErr(err)
|
return dbErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Let's just be lazy for now, optimize later if need be.
|
||||||
|
if mode == 0 || mode&models.SMTags != 0 {
|
||||||
|
_, err = tx.ExecContext(ctx, "DELETE FROM device_tag WHERE device_id=?", record.ID)
|
||||||
|
if err != nil {
|
||||||
|
return dbErr(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode == 0 || mode&models.SMProperties != 0 {
|
||||||
|
_, 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)
|
||||||
|
devices[i].ID = int(lastID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode == 0 || mode&models.SMTags != 0 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if mode == 0 || mode&models.SMProperties != 0 {
|
if mode == 0 || mode&models.SMProperties != 0 {
|
||||||
_, err = tx.ExecContext(ctx, "DELETE FROM device_property WHERE device_id=?", record.ID)
|
for key, value := range device.UserProperties {
|
||||||
if err != nil {
|
_, err := tx.ExecContext(ctx, "INSERT INTO device_property (device_id, prop_key, prop_value, is_user) VALUES (?, ?, ?, 1)",
|
||||||
return dbErr(err)
|
record.ID, key, value,
|
||||||
|
)
|
||||||
|
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()
|
for key, value := range device.DriverProperties {
|
||||||
if err != nil {
|
j, err := json.Marshal(value)
|
||||||
return dbErr(err)
|
if err != nil {
|
||||||
}
|
// Eh, it'll get filled by the driver anyway
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
record.ID = int(lastID)
|
_, err = tx.ExecContext(ctx, "INSERT INTO device_property (device_id, prop_key, prop_value, is_user) VALUES (?, ?, ?, 0)",
|
||||||
device.ID = int(lastID)
|
record.ID, key, string(j),
|
||||||
}
|
)
|
||||||
|
if err != nil {
|
||||||
if mode == 0 || mode&models.SMTags != 0 {
|
// Return err here anyway, it might put the tx in a bad state to ignore it.
|
||||||
for _, tag := range device.Tags {
|
return dbErr(err)
|
||||||
_, err := tx.ExecContext(ctx, "INSERT INTO device_tag (device_id, tag_name) VALUES (?, ?)", record.ID, tag)
|
}
|
||||||
if err != nil {
|
|
||||||
return dbErr(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if mode == 0 || mode&models.SMProperties != 0 {
|
|
||||||
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 {
|
if mode == 0 || mode&models.SMState != 0 {
|
||||||
j, err := json.Marshal(value)
|
_, err = tx.NamedExecContext(ctx, `
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if mode == 0 || mode&models.SMState != 0 {
|
|
||||||
_, err = tx.NamedExecContext(ctx, `
|
|
||||||
REPLACE INTO device_state(device_id, hue, saturation, kelvin, power, intensity)
|
REPLACE INTO device_state(device_id, hue, saturation, kelvin, power, intensity)
|
||||||
VALUES (:device_id, :hue, :saturation, :kelvin, :power, :intensity)
|
VALUES (:device_id, :hue, :saturation, :kelvin, :power, :intensity)
|
||||||
`, deviceStateRecord{
|
`, deviceStateRecord{
|
||||||
DeviceID: record.ID,
|
DeviceID: record.ID,
|
||||||
Hue: device.State.Color.Hue,
|
Hue: device.State.Color.Hue,
|
||||||
Saturation: device.State.Color.Saturation,
|
Saturation: device.State.Color.Saturation,
|
||||||
Kelvin: device.State.Color.Kelvin,
|
Kelvin: device.State.Color.Kelvin,
|
||||||
Power: device.State.Power,
|
Power: device.State.Power,
|
||||||
Intensity: device.State.Intensity,
|
Intensity: device.State.Intensity,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return dbErr(err)
|
return dbErr(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *DeviceRepo) Save(ctx context.Context, device *models.Device, mode models.SaveMode) error {
|
||||||
|
devices := []models.Device{*device}
|
||||||
|
|
||||||
|
err := r.SaveMany(ctx, mode, devices)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*device = devices[0]
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *DeviceRepo) Delete(ctx context.Context, device *models.Device) error {
|
func (r *DeviceRepo) Delete(ctx context.Context, device *models.Device) error {
|
||||||
_, err := r.DBX.ExecContext(ctx, "DELETE FROM device WHERE Id=?", device.ID)
|
_, err := r.DBX.ExecContext(ctx, "DELETE FROM device WHERE Id=?", device.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -328,12 +349,15 @@ func (r *DeviceRepo) populate(ctx context.Context, records []deviceRecord) ([]mo
|
|||||||
InternalID: record.InternalID,
|
InternalID: record.InternalID,
|
||||||
Icon: record.Icon,
|
Icon: record.Icon,
|
||||||
Name: record.Name,
|
Name: record.Name,
|
||||||
|
SceneAssignments: make([]models.DeviceSceneAssignment, 0, 4),
|
||||||
ButtonNames: strings.Split(record.ButtonNames, ","),
|
ButtonNames: strings.Split(record.ButtonNames, ","),
|
||||||
DriverProperties: make(map[string]interface{}, 8),
|
DriverProperties: make(map[string]interface{}, 8),
|
||||||
UserProperties: make(map[string]string, 8),
|
UserProperties: make(map[string]string, 8),
|
||||||
Tags: make([]string, 0, 8),
|
Tags: make([]string, 0, 8),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = json.Unmarshal(record.SceneAssignmentJSON, &device.SceneAssignments)
|
||||||
|
|
||||||
if device.ButtonNames[0] == "" {
|
if device.ButtonNames[0] == "" {
|
||||||
device.ButtonNames = device.ButtonNames[:0]
|
device.ButtonNames = device.ButtonNames[:0]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"git.aiterp.net/lucifer/new-server/models"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sceneRecord struct {
|
||||||
|
ID int `db:"id"`
|
||||||
|
Name string `db:"name"`
|
||||||
|
IntervalMS int64 `db:"interval_ms"`
|
||||||
|
RoleJSON json.RawMessage `db:"roles"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SceneRepo struct {
|
||||||
|
DBX *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SceneRepo) Find(ctx context.Context, id int) (*models.Scene, error) {
|
||||||
|
var scene sceneRecord
|
||||||
|
err := r.DBX.GetContext(ctx, &scene, "SELECT * FROM scene WHERE id = ?", id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, dbErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.populateOne(&scene)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SceneRepo) FetchAll(ctx context.Context) ([]models.Scene, error) {
|
||||||
|
scenes := make([]sceneRecord, 0, 8)
|
||||||
|
err := r.DBX.SelectContext(ctx, &scenes, "SELECT * FROM scene")
|
||||||
|
if err != nil {
|
||||||
|
return nil, dbErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.populate(scenes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SceneRepo) Save(ctx context.Context, scene *models.Scene) error {
|
||||||
|
j, err := json.Marshal(scene.Roles)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if scene.ID > 0 {
|
||||||
|
_, err := r.DBX.ExecContext(
|
||||||
|
ctx,
|
||||||
|
"UPDATE scene SET name = ?, interval_ms = ?, roles = ? WHERE id = ?",
|
||||||
|
scene.Name, scene.IntervalMS, j, scene.ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return dbErr(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rs, err := r.DBX.ExecContext(
|
||||||
|
ctx,
|
||||||
|
"INSERT INTO scene (name, interval_ms, roles) VALUES (?, ?, ?)",
|
||||||
|
scene.Name, scene.IntervalMS, j,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return dbErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := rs.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
return dbErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scene.ID = int(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SceneRepo) Delete(ctx context.Context, scene *models.Scene) error {
|
||||||
|
_, err := r.DBX.ExecContext(ctx, "DELETE FROM scene WHERE id = ?", scene.ID)
|
||||||
|
if err != nil {
|
||||||
|
return dbErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scene.ID = 0
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SceneRepo) populateOne(record *sceneRecord) (*models.Scene, error) {
|
||||||
|
records, err := r.populate([]sceneRecord{*record})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &records[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SceneRepo) populate(records []sceneRecord) ([]models.Scene, error) {
|
||||||
|
res := make([]models.Scene, 0, len(records))
|
||||||
|
|
||||||
|
for _, record := range records {
|
||||||
|
scene := models.Scene{
|
||||||
|
ID: record.ID,
|
||||||
|
Name: record.Name,
|
||||||
|
IntervalMS: record.IntervalMS,
|
||||||
|
Roles: make([]models.SceneRole, 0, 8),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.Unmarshal(record.RoleJSON, &scene.Roles)
|
||||||
|
if err != nil {
|
||||||
|
return nil, dbErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res = append(res, scene)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
+13
-11
@@ -7,17 +7,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Device struct {
|
type Device struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
BridgeID int `json:"bridgeID"`
|
BridgeID int `json:"bridgeID"`
|
||||||
InternalID string `json:"internalId"`
|
InternalID string `json:"internalId"`
|
||||||
Icon string `json:"icon"`
|
Icon string `json:"icon"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Capabilities []DeviceCapability `json:"capabilities"`
|
Capabilities []DeviceCapability `json:"capabilities"`
|
||||||
ButtonNames []string `json:"buttonNames"`
|
ButtonNames []string `json:"buttonNames"`
|
||||||
DriverProperties map[string]interface{} `json:"driverProperties"`
|
DriverProperties map[string]interface{} `json:"driverProperties"`
|
||||||
UserProperties map[string]string `json:"userProperties"`
|
UserProperties map[string]string `json:"userProperties"`
|
||||||
State DeviceState `json:"state"`
|
SceneAssignments []DeviceSceneAssignment `json:"sceneAssignments"`
|
||||||
Tags []string `json:"tags"`
|
State DeviceState `json:"state"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeviceUpdate struct {
|
type DeviceUpdate struct {
|
||||||
@@ -66,6 +67,7 @@ type DeviceRepository interface {
|
|||||||
Find(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, mode SaveMode) error
|
Save(ctx context.Context, device *Device, mode SaveMode) error
|
||||||
|
SaveMany(ctx context.Context, mode SaveMode, devices []Device) error
|
||||||
Delete(ctx context.Context, device *Device) error
|
Delete(ctx context.Context, device *Device) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+53
-12
@@ -1,10 +1,12 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"math"
|
"math"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Scene struct {
|
type Scene struct {
|
||||||
@@ -54,15 +56,16 @@ func (s *Scene) RoleIndex(device *Device) int {
|
|||||||
type SceneEffect string
|
type SceneEffect string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SEStatic SceneEffect = "Plain"
|
SEStatic SceneEffect = "Plain"
|
||||||
SERandom SceneEffect = "Random"
|
SERandom SceneEffect = "Random"
|
||||||
SEGradient SceneEffect = "Gradient"
|
SEGradient SceneEffect = "Gradient"
|
||||||
SETransition SceneEffect = "Transition"
|
SEWalkingGradient SceneEffect = "WalkingGradient"
|
||||||
|
SETransition SceneEffect = "Transition"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SceneRole struct {
|
type SceneRole struct {
|
||||||
Effect SceneEffect `json:"effect"`
|
Effect SceneEffect `json:"effect"`
|
||||||
PowerMode ScenePowerMode `json:"overridePower"`
|
PowerMode ScenePowerMode `json:"powerMode"`
|
||||||
TargetKind ReferenceKind `json:"targetKind"`
|
TargetKind ReferenceKind `json:"targetKind"`
|
||||||
TargetValue string `json:"targetValue"`
|
TargetValue string `json:"targetValue"`
|
||||||
Interpolate bool `json:"interpolate"`
|
Interpolate bool `json:"interpolate"`
|
||||||
@@ -70,6 +73,34 @@ type SceneRole struct {
|
|||||||
States []NewDeviceState `json:"states"`
|
States []NewDeviceState `json:"states"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SceneRunContext struct {
|
||||||
|
Index int
|
||||||
|
Length int
|
||||||
|
IntervalNumber int64
|
||||||
|
IntervalMax int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SceneRunContext) PositionFacShifted() float64 {
|
||||||
|
shiftFac := float64((int64(d.Index)+d.IntervalNumber)%int64(d.Length)) * (1 / float64(d.Length))
|
||||||
|
|
||||||
|
return math.Mod(d.PositionFac()+shiftFac, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SceneRunContext) PositionFac() float64 {
|
||||||
|
return float64(d.Index) / float64(d.Length)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SceneRunContext) IntervalFac() float64 {
|
||||||
|
fac := float64(d.IntervalNumber) / float64(d.IntervalMax)
|
||||||
|
if fac > 1 {
|
||||||
|
return 1
|
||||||
|
} else if fac < 0 {
|
||||||
|
return 0
|
||||||
|
} else {
|
||||||
|
return fac
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (r *SceneRole) Validate() error {
|
func (r *SceneRole) Validate() error {
|
||||||
if len(r.States) == 0 {
|
if len(r.States) == 0 {
|
||||||
return ErrSceneRoleNoStates
|
return ErrSceneRoleNoStates
|
||||||
@@ -134,16 +165,18 @@ func (r *SceneRole) ApplyOrder(devices []Device) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *SceneRole) ApplyEffect(device *Device, intervalFac float64, positionFac float64) (newState NewDeviceState, deviceChanged bool) {
|
func (r *SceneRole) ApplyEffect(device *Device, c SceneRunContext) (newState NewDeviceState, deviceChanged bool) {
|
||||||
switch r.Effect {
|
switch r.Effect {
|
||||||
case SEStatic:
|
case SEStatic:
|
||||||
newState = r.States[0]
|
newState = r.States[0]
|
||||||
case SERandom:
|
case SERandom:
|
||||||
newState = r.State(rand.Float64())
|
newState = r.State(rand.Float64())
|
||||||
case SEGradient:
|
case SEGradient:
|
||||||
newState = r.State(positionFac)
|
newState = r.State(c.PositionFac())
|
||||||
|
case SEWalkingGradient:
|
||||||
|
newState = r.State(c.PositionFacShifted())
|
||||||
case SETransition:
|
case SETransition:
|
||||||
newState = r.State(intervalFac)
|
newState = r.State(c.IntervalFac())
|
||||||
}
|
}
|
||||||
|
|
||||||
switch r.PowerMode {
|
switch r.PowerMode {
|
||||||
@@ -196,9 +229,17 @@ const (
|
|||||||
SPOverride ScenePowerMode = "Override" // Same as above, but Scene state set Device state' power. This is good for "wake up" scenes.
|
SPOverride ScenePowerMode = "Override" // Same as above, but Scene state set Device state' power. This is good for "wake up" scenes.
|
||||||
)
|
)
|
||||||
|
|
||||||
type SceneAssignment struct {
|
// DeviceSceneAssignment is an entry on the device stack.
|
||||||
Scene *Scene
|
type DeviceSceneAssignment struct {
|
||||||
Device *Device
|
SceneID int `json:"sceneId"`
|
||||||
Tag string
|
Group string `json:"group"`
|
||||||
|
StartTime time.Time `json:"start"`
|
||||||
|
DurationMS int64 `json:"durationMs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SceneRepository interface {
|
||||||
|
Find(ctx context.Context, id int) (*Scene, error)
|
||||||
|
FetchAll(ctx context.Context) ([]Scene, error)
|
||||||
|
Save(ctx context.Context, bridge *Scene) error
|
||||||
|
Delete(ctx context.Context, bridge *Scene) error
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE scene
|
||||||
|
(
|
||||||
|
id INT NOT NULL AUTO_INCREMENT,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
interval_ms INT NOT NULL,
|
||||||
|
roles JSON NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE scene;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
ALTER TABLE device ADD COLUMN scene_assignments JSON NOT NULL DEFAULT ('[]');
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
ALTER TABLE device DROP COLUMN IF EXISTS scene_assignments;
|
||||||
|
-- +goose StatementEnd
|
||||||
Reference in New Issue
Block a user