Compare commits
2
Commits
7b0028ee2b
..
3.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d91bd472dd | ||
|
|
251687b85d |
@@ -5,9 +5,7 @@ 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"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
"log"
|
"log"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func fetchDevices(ctx context.Context, fetchStr string) ([]models.Device, error) {
|
func fetchDevices(ctx context.Context, fetchStr string) ([]models.Device, error) {
|
||||||
@@ -196,50 +194,4 @@ 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
|
|
||||||
}
|
|
||||||
|
|
||||||
_, 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}
|
|
||||||
}
|
|
||||||
config.PublishChannel <- devices
|
|
||||||
|
|
||||||
eg := errgroup.Group{}
|
|
||||||
for i := range devices {
|
|
||||||
device := devices[i]
|
|
||||||
|
|
||||||
eg.Go(func() error {
|
|
||||||
return config.DeviceRepository().Save(ctxOf(c), &device, 0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
err = eg.Wait()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return devices, nil
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-6
@@ -35,17 +35,28 @@ func EventHandlers(r gin.IRoutes) {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
r.PUT("/:id", handler(func(c *gin.Context) (interface{}, error) {
|
r.PUT("/:id", handler(func(c *gin.Context) (interface{}, error) {
|
||||||
var body models.EventHandlerUpdate
|
|
||||||
err := parseBody(c, &body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
handler, err := config.EventHandlerRepository().FindByID(ctxOf(c), intParam(c, "id"))
|
handler, err := config.EventHandlerRepository().FindByID(ctxOf(c), intParam(c, "id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
handler.ApplyUpdate(body)
|
|
||||||
|
if c.Query("hard") == "true" {
|
||||||
|
var body models.EventHandler
|
||||||
|
err := parseBody(c, &body)
|
||||||
|
if err != nil || body.ID != intParam(c, "id") {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
handler = &body
|
||||||
|
} else {
|
||||||
|
var body models.EventHandlerUpdate
|
||||||
|
err := parseBody(c, &body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
handler.ApplyUpdate(body)
|
||||||
|
}
|
||||||
|
|
||||||
err = config.EventHandlerRepository().Save(ctxOf(c), handler)
|
err = config.EventHandlerRepository().Save(ctxOf(c), handler)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
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
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
@@ -15,13 +15,6 @@ var errorMap = map[error]int{
|
|||||||
models.ErrBadColor: 400,
|
models.ErrBadColor: 400,
|
||||||
models.ErrInternal: 500,
|
models.ErrInternal: 500,
|
||||||
models.ErrUnknownColorFormat: 400,
|
models.ErrUnknownColorFormat: 400,
|
||||||
|
|
||||||
models.ErrSceneInvalidInterval: 400,
|
|
||||||
models.ErrSceneNoRoles: 400,
|
|
||||||
models.ErrSceneRoleNoStates: 400,
|
|
||||||
models.ErrSceneRoleUnsupportedOrdering: 422,
|
|
||||||
models.ErrSceneRoleUnknownEffect: 422,
|
|
||||||
models.ErrSceneRoleUnknownPowerMode: 422,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type response struct {
|
type response struct {
|
||||||
|
|||||||
@@ -16,6 +16,26 @@ func (client *Client) GetHandlers(ctx context.Context) ([]models.EventHandler, e
|
|||||||
return handlers, nil
|
return handlers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (client *Client) GetHandler(ctx context.Context, id int) (*models.EventHandler, error) {
|
||||||
|
var handler models.EventHandler
|
||||||
|
err := client.Fetch(ctx, "GET", fmt.Sprintf("/api/event-handlers/%d", id), &handler, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &handler, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) PutHandler(ctx context.Context, handler *models.EventHandler) (*models.EventHandler, error) {
|
||||||
|
var response models.EventHandler
|
||||||
|
err := client.Fetch(ctx, "PUT", fmt.Sprintf("/api/event-handlers/%d", handler.ID), &response, handler)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (client *Client) AddHandler(ctx context.Context, handler models.EventHandler) (*models.EventHandler, error) {
|
func (client *Client) AddHandler(ctx context.Context, handler models.EventHandler) (*models.EventHandler, error) {
|
||||||
var result models.EventHandler
|
var result models.EventHandler
|
||||||
err := client.Fetch(ctx, "POST", "/api/event-handlers", &result, handler)
|
err := client.Fetch(ctx, "POST", "/api/event-handlers", &result, handler)
|
||||||
|
|||||||
+2
-3
@@ -24,9 +24,8 @@ func DBX() *sqlx.DB {
|
|||||||
MySqlSchema(),
|
MySqlSchema(),
|
||||||
))
|
))
|
||||||
|
|
||||||
dbx.SetMaxIdleConns(20)
|
dbx.SetMaxIdleConns(50)
|
||||||
dbx.SetMaxOpenConns(40)
|
dbx.SetMaxOpenConns(100)
|
||||||
dbx.SetConnMaxLifetime(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return dbx
|
return dbx
|
||||||
|
|||||||
@@ -20,7 +20,3 @@ 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
-13
@@ -1,28 +1,17 @@
|
|||||||
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/publisher"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func StartServer() {
|
func StartServer() {
|
||||||
setupCtx, cancel := context.WithTimeout(context.Background(), time.Second * 10)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
err := publisher.Initialize(setupCtx)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("Publish init failed:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
services.StartEventHandler()
|
services.StartEventHandler()
|
||||||
|
services.StartPublisher()
|
||||||
services.ConnectToBridges()
|
services.ConnectToBridges()
|
||||||
services.CheckNewDevices()
|
services.CheckNewDevices()
|
||||||
|
|
||||||
@@ -36,7 +25,6 @@ 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())))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,19 +160,18 @@ func handleEvent(event models.Event) (responses []models.Event) {
|
|||||||
config.PublishChannel <- allDevices
|
config.PublishChannel <- allDevices
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
|
for _, device := range allDevices {
|
||||||
if len(allDevices) > 0 {
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
|
||||||
err := config.DeviceRepository().SaveMany(context.Background(), models.SMState, allDevices)
|
go func(device models.Device) {
|
||||||
|
err := config.DeviceRepository().Save(context.Background(), &device, models.SMState)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Failed to save devices' state:", err)
|
log.Println("Failed to save device for state:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}(device)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, handler := range deadHandlers {
|
for _, handler := range deadHandlers {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"git.aiterp.net/lucifer/new-server/app/config"
|
||||||
|
"git.aiterp.net/lucifer/new-server/models"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StartPublisher() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for devices := range config.PublishChannel {
|
||||||
|
if len(devices) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
lists := make(map[int][]models.Device, 4)
|
||||||
|
for _, device := range devices {
|
||||||
|
lists[device.BridgeID] = append(lists[device.BridgeID], device)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, time.Second * 30)
|
||||||
|
|
||||||
|
bridges, err := config.BridgeRepository().FetchAll(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Publishing error (1): " + err.Error())
|
||||||
|
cancel()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
for _, devices := range lists {
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
@@ -1,275 +0,0 @@
|
|||||||
package publisher
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"git.aiterp.net/lucifer/new-server/app/config"
|
|
||||||
"git.aiterp.net/lucifer/new-server/models"
|
|
||||||
"log"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Publisher struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
sceneData map[int]*models.Scene
|
|
||||||
scenes []*Scene
|
|
||||||
sceneAssignment map[int]*Scene
|
|
||||||
started map[int]bool
|
|
||||||
pending map[int][]models.Device
|
|
||||||
waiting map[int]chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Publisher) ReloadScenes(ctx context.Context) error {
|
|
||||||
scenes, err := config.SceneRepository().FetchAll(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
p.mu.Lock()
|
|
||||||
for _, scene := range scenes {
|
|
||||||
p.sceneData[scene.ID] = &scene
|
|
||||||
}
|
|
||||||
p.mu.Unlock()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Publisher) ReloadDevices(ctx context.Context) error {
|
|
||||||
devices, err := config.DeviceRepository().FetchByReference(ctx, models.RKAll, "")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
p.Publish(devices...)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Publisher) Publish(devices ...models.Device) {
|
|
||||||
if len(devices) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
|
|
||||||
for _, device := range devices {
|
|
||||||
if !p.started[device.BridgeID] {
|
|
||||||
p.started[device.BridgeID] = true
|
|
||||||
go p.runBridge(device.BridgeID)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.reassignDevice(device)
|
|
||||||
|
|
||||||
if p.sceneAssignment[device.ID] != nil {
|
|
||||||
p.sceneAssignment[device.ID].UpsertDevice(device)
|
|
||||||
} else {
|
|
||||||
p.pending[device.BridgeID] = append(p.pending[device.BridgeID], device)
|
|
||||||
if p.waiting[device.BridgeID] != nil {
|
|
||||||
close(p.waiting[device.BridgeID])
|
|
||||||
p.waiting[device.BridgeID] = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Publisher) PublishChannel(ch <-chan []models.Device) {
|
|
||||||
for list := range ch {
|
|
||||||
p.Publish(list...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Publisher) Run() {
|
|
||||||
ticker := time.NewTicker(time.Millisecond * 100)
|
|
||||||
deleteList := make([]int, 0, 8)
|
|
||||||
updatedList := make([]models.Device, 0, 16)
|
|
||||||
|
|
||||||
for range ticker.C {
|
|
||||||
deleteList = deleteList[:0]
|
|
||||||
updatedList = updatedList[:0]
|
|
||||||
|
|
||||||
p.mu.Lock()
|
|
||||||
for i, scene := range p.scenes {
|
|
||||||
if !scene.endTime.IsZero() && time.Now().After(scene.endTime) {
|
|
||||||
deleteList = append(deleteList, i-len(deleteList))
|
|
||||||
updatedList = append(updatedList, scene.AllDevices()...)
|
|
||||||
|
|
||||||
for _, device := range scene.AllDevices() {
|
|
||||||
p.sceneAssignment[device.ID] = nil
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if scene.Due() {
|
|
||||||
updatedList = append(updatedList, scene.Run()...)
|
|
||||||
updatedList = append(updatedList, scene.UnaffectedDevices()...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, i := range deleteList {
|
|
||||||
p.scenes = append(p.scenes[:i], p.scenes[i+1:]...)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, device := range updatedList {
|
|
||||||
if !p.started[device.BridgeID] {
|
|
||||||
p.started[device.BridgeID] = true
|
|
||||||
go p.runBridge(device.BridgeID)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.pending[device.BridgeID] = append(p.pending[device.BridgeID], device)
|
|
||||||
if p.waiting[device.BridgeID] != nil {
|
|
||||||
close(p.waiting[device.BridgeID])
|
|
||||||
p.waiting[device.BridgeID] = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p.mu.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// reassignDevice re-evaluates the device's scene assignment config. It will return whether the scene changed, which
|
|
||||||
// should trigger an update.
|
|
||||||
func (p *Publisher) reassignDevice(device models.Device) bool {
|
|
||||||
var selectedAssignment *models.DeviceSceneAssignment
|
|
||||||
for _, assignment := range device.SceneAssignments {
|
|
||||||
duration := time.Duration(assignment.DurationMS) * time.Millisecond
|
|
||||||
if duration <= 0 || time.Now().Before(assignment.StartTime.Add(duration)) {
|
|
||||||
selectedAssignment = &assignment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if selectedAssignment == nil {
|
|
||||||
if p.sceneAssignment[device.ID] != nil {
|
|
||||||
p.sceneAssignment[device.ID].RemoveDevice(device)
|
|
||||||
delete(p.sceneAssignment, device.ID)
|
|
||||||
|
|
||||||
// Scene changed
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop here, no scene should be assigned.
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.sceneAssignment[device.ID] != nil {
|
|
||||||
scene := p.sceneAssignment[device.ID]
|
|
||||||
if scene.data.ID == selectedAssignment.SceneID && scene.group == selectedAssignment.Group {
|
|
||||||
// Current assignment is good.
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
p.sceneAssignment[device.ID].RemoveDevice(device)
|
|
||||||
delete(p.sceneAssignment, device.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, scene := range p.scenes {
|
|
||||||
if scene.data.ID == selectedAssignment.SceneID && scene.group == selectedAssignment.Group {
|
|
||||||
p.sceneAssignment[device.ID] = scene
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newScene := &Scene{
|
|
||||||
data: p.sceneData[selectedAssignment.SceneID],
|
|
||||||
group: selectedAssignment.Group,
|
|
||||||
startTime: selectedAssignment.StartTime,
|
|
||||||
endTime: selectedAssignment.StartTime.Add(time.Duration(selectedAssignment.DurationMS) * time.Millisecond),
|
|
||||||
roleMap: make(map[int]int, 16),
|
|
||||||
roleList: make(map[int][]models.Device, 16),
|
|
||||||
due: true,
|
|
||||||
}
|
|
||||||
p.sceneAssignment[device.ID] = newScene
|
|
||||||
p.scenes = append(p.scenes, newScene)
|
|
||||||
|
|
||||||
if selectedAssignment.DurationMS <= 0 {
|
|
||||||
newScene.endTime = time.Time{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Publisher) runBridge(id int) {
|
|
||||||
defer func() {
|
|
||||||
p.mu.Lock()
|
|
||||||
p.started[id] = false
|
|
||||||
p.mu.Unlock()
|
|
||||||
}()
|
|
||||||
|
|
||||||
bridge, err := config.BridgeRepository().Find(context.Background(), id)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to get bridge data:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
driver, err := config.DriverProvider().Provide(bridge.Driver)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to get bridge driver:", err)
|
|
||||||
log.Println("Maybe Lucifer needs to be updated.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
devices := make(map[int]models.Device)
|
|
||||||
|
|
||||||
for {
|
|
||||||
p.mu.Lock()
|
|
||||||
if len(p.pending[id]) == 0 {
|
|
||||||
if p.waiting[id] == nil {
|
|
||||||
p.waiting[id] = make(chan struct{})
|
|
||||||
}
|
|
||||||
waitCh := p.waiting[id]
|
|
||||||
p.mu.Unlock()
|
|
||||||
<-waitCh
|
|
||||||
p.mu.Lock()
|
|
||||||
}
|
|
||||||
|
|
||||||
updates := p.pending[id]
|
|
||||||
p.pending[id] = p.pending[id][:0:0]
|
|
||||||
p.mu.Unlock()
|
|
||||||
|
|
||||||
// Only allow the latest update per device (this avoids slow bridges causing a backlog of cations).
|
|
||||||
for key := range devices {
|
|
||||||
delete(devices, key)
|
|
||||||
}
|
|
||||||
for _, update := range updates {
|
|
||||||
devices[update.ID] = update
|
|
||||||
}
|
|
||||||
updates = updates[:0]
|
|
||||||
for _, value := range devices {
|
|
||||||
updates = append(updates, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := driver.Publish(context.Background(), bridge, updates)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to publish to driver:", err)
|
|
||||||
|
|
||||||
p.mu.Lock()
|
|
||||||
p.pending[id] = append(updates, p.pending[id]...)
|
|
||||||
p.mu.Unlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var publisher = Publisher{
|
|
||||||
sceneData: make(map[int]*models.Scene),
|
|
||||||
scenes: make([]*Scene, 0, 16),
|
|
||||||
sceneAssignment: make(map[int]*Scene, 16),
|
|
||||||
started: make(map[int]bool),
|
|
||||||
pending: make(map[int][]models.Device),
|
|
||||||
waiting: make(map[int]chan struct{}),
|
|
||||||
}
|
|
||||||
|
|
||||||
func Initialize(ctx context.Context) error {
|
|
||||||
err :=publisher.ReloadScenes(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = publisher.ReloadDevices(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
go publisher.PublishChannel(config.PublishChannel)
|
|
||||||
go publisher.Run()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
package publisher
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.aiterp.net/lucifer/new-server/models"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Scene struct {
|
|
||||||
data *models.Scene
|
|
||||||
group string
|
|
||||||
startTime time.Time
|
|
||||||
endTime time.Time
|
|
||||||
roleMap map[int]int
|
|
||||||
roleList map[int][]models.Device
|
|
||||||
|
|
||||||
due bool
|
|
||||||
lastInterval int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateScene updates the scene data and re-seats all devices.
|
|
||||||
func (s *Scene) UpdateScene(data models.Scene) {
|
|
||||||
devices := make([]models.Device, 0, 16)
|
|
||||||
|
|
||||||
// Collect all devices into the undefined role (-1)
|
|
||||||
for _, list := range s.roleList {
|
|
||||||
for _, device := range list {
|
|
||||||
devices = append(devices, device)
|
|
||||||
s.roleMap[device.ID] = -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.roleList = map[int][]models.Device{-1: devices}
|
|
||||||
|
|
||||||
// Update data and reset devices.
|
|
||||||
s.data = &data
|
|
||||||
for _, device := range append(devices[:0:0], devices...) {
|
|
||||||
s.UpsertDevice(device)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpsertDevice moves the device if necessary and updates its state.
|
|
||||||
func (s *Scene) UpsertDevice(device models.Device) {
|
|
||||||
if s.data == nil {
|
|
||||||
s.roleMap[device.ID] = -1
|
|
||||||
s.roleList[-1] = append(s.roleList[-1], device)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
oldIndex, hasOldIndex := s.roleMap[device.ID]
|
|
||||||
newIndex := s.data.RoleIndex(&device)
|
|
||||||
|
|
||||||
s.roleMap[device.ID] = newIndex
|
|
||||||
|
|
||||||
if hasOldIndex {
|
|
||||||
for i, device2 := range s.roleList[oldIndex] {
|
|
||||||
if device2.ID == device.ID {
|
|
||||||
s.roleList[oldIndex] = append(s.roleList[oldIndex][:i], s.roleList[oldIndex][i+1:]...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s.due = true
|
|
||||||
|
|
||||||
s.roleList[newIndex] = append(s.roleList[newIndex], device)
|
|
||||||
if newIndex != -1 {
|
|
||||||
s.data.Roles[newIndex].ApplyOrder(s.roleList[newIndex])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveDevice finds and remove a device. It's a noop if the device does not exist in this scene.
|
|
||||||
func (s *Scene) RemoveDevice(device models.Device) {
|
|
||||||
roleIndex, hasRoleIndex := s.roleMap[device.ID]
|
|
||||||
if !hasRoleIndex {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, device2 := range s.roleList[roleIndex] {
|
|
||||||
if device2.ID == device.ID {
|
|
||||||
s.roleList[roleIndex] = append(s.roleList[roleIndex][:i], s.roleList[roleIndex][i+1:]...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s.due = true
|
|
||||||
|
|
||||||
delete(s.roleMap, device.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Scene) Due() bool {
|
|
||||||
if s.due {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.data.IntervalMS > 0 {
|
|
||||||
interval := time.Duration(s.data.IntervalMS) * time.Millisecond
|
|
||||||
return int64(time.Since(s.startTime)/interval) != s.lastInterval
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Scene) UnaffectedDevices() []models.Device {
|
|
||||||
return append(s.roleList[-1][:0:0], s.roleList[-1]...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Scene) AllDevices() []models.Device {
|
|
||||||
res := make([]models.Device, 0, 16)
|
|
||||||
for _, list := range s.roleList {
|
|
||||||
res = append(res, list...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run runs the scene
|
|
||||||
func (s *Scene) Run() []models.Device {
|
|
||||||
if s.data == nil {
|
|
||||||
return []models.Device{}
|
|
||||||
}
|
|
||||||
|
|
||||||
intervalNumber := int64(0)
|
|
||||||
intervalMax := int64(1)
|
|
||||||
if s.data.IntervalMS > 0 {
|
|
||||||
interval := time.Duration(s.data.IntervalMS) * time.Millisecond
|
|
||||||
intervalNumber = int64(time.Since(s.startTime) / interval)
|
|
||||||
|
|
||||||
if !s.endTime.IsZero() {
|
|
||||||
intervalMax = int64(s.endTime.Sub(s.startTime) / interval)
|
|
||||||
} else {
|
|
||||||
intervalMax = intervalNumber + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedDevices := make([]models.Device, 0, 16)
|
|
||||||
for i, list := range s.roleList {
|
|
||||||
if i == -1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
role := s.data.Roles[i]
|
|
||||||
|
|
||||||
for j, device := range list {
|
|
||||||
newState := role.ApplyEffect(&device, models.SceneRunContext{
|
|
||||||
Index: j,
|
|
||||||
Length: len(list),
|
|
||||||
IntervalNumber: intervalNumber,
|
|
||||||
IntervalMax: intervalMax,
|
|
||||||
})
|
|
||||||
|
|
||||||
err := device.SetState(newState)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedDevices = append(updatedDevices, device)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s.due = false
|
|
||||||
s.lastInterval = intervalNumber
|
|
||||||
|
|
||||||
return updatedDevices
|
|
||||||
}
|
|
||||||
+110
-41
@@ -7,6 +7,7 @@ import (
|
|||||||
"git.aiterp.net/lucifer/new-server/models"
|
"git.aiterp.net/lucifer/new-server/models"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handlerCmd(
|
func handlerCmd(
|
||||||
@@ -38,47 +39,7 @@ func handlerCmd(
|
|||||||
log.Fatalln("Search filter needed!")
|
log.Fatalln("Search filter needed!")
|
||||||
}
|
}
|
||||||
model.TargetKind, model.TargetValue = models.ParseFetchString(*fetchStr)
|
model.TargetKind, model.TargetValue = models.ParseFetchString(*fetchStr)
|
||||||
|
model = applyCmdToHandler(model, cmd)
|
||||||
// From / To
|
|
||||||
model.From = cmd.Params.Get("from").TimeOfDayOr(models.Never)
|
|
||||||
model.To = cmd.Params.Get("to").TimeOfDayOr(models.Never)
|
|
||||||
if model.From.IsNever() != model.To.IsNever() {
|
|
||||||
log.Fatalln("from and to must be specified together!")
|
|
||||||
}
|
|
||||||
|
|
||||||
// One shot
|
|
||||||
model.OneShot = cmd.Params.Get("one-shot").StringOr("false") == "true"
|
|
||||||
|
|
||||||
// Priority
|
|
||||||
model.Priority = cmd.Params.Get("priority").IntOr(100)
|
|
||||||
|
|
||||||
// Conditions
|
|
||||||
model.Conditions = cmd.Params.Subset("conditions").EventConditions()
|
|
||||||
|
|
||||||
// Power, Color, Intensity, Temperature
|
|
||||||
nds := cmd.Params.DeviceState("set-")
|
|
||||||
model.Actions.SetPower = nds.Power
|
|
||||||
model.Actions.SetColor = nds.Color
|
|
||||||
model.Actions.SetIntensity = nds.Intensity
|
|
||||||
|
|
||||||
// Add intensity
|
|
||||||
model.Actions.AddIntensity = cmd.Params.Get("add-intensity").Float()
|
|
||||||
|
|
||||||
// Fire
|
|
||||||
fireParams := cmd.Params.Subset("fire")
|
|
||||||
fireName := fireParams.Get("name").String()
|
|
||||||
if fireName != nil {
|
|
||||||
firePayloadParams := fireParams.Subset("payload")
|
|
||||||
|
|
||||||
model.Actions.FireEvent = &models.Event{
|
|
||||||
Name: *fireName,
|
|
||||||
Payload: make(map[string]string, len(firePayloadParams)),
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, value := range firePayloadParams.StringMap() {
|
|
||||||
model.Actions.FireEvent.Payload[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newHandler, err := c.AddHandler(ctx, model)
|
newHandler, err := c.AddHandler(ctx, model)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -86,6 +47,34 @@ func handlerCmd(
|
|||||||
}
|
}
|
||||||
|
|
||||||
WriteHandlerInfoTable(os.Stdout, []models.EventHandler{*newHandler})
|
WriteHandlerInfoTable(os.Stdout, []models.EventHandler{*newHandler})
|
||||||
|
case "edit":
|
||||||
|
id := cmd.Params.Get(0).IntOr(0)
|
||||||
|
if id == 0 {
|
||||||
|
log.Fatalln("ID missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
model, err := c.GetHandler(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
eventStr := cmd.Params.Get("event").String()
|
||||||
|
if eventStr != nil {
|
||||||
|
model.EventName = *eventStr
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchStr := cmd.Params.Get("search").String()
|
||||||
|
if fetchStr != nil {
|
||||||
|
model.TargetKind, model.TargetValue = models.ParseFetchString(*fetchStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
modified := applyCmdToHandler(*model, cmd)
|
||||||
|
returned, err := c.PutHandler(ctx, &modified)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteHandlerInfoTable(os.Stdout, []models.EventHandler{*returned})
|
||||||
case "delete":
|
case "delete":
|
||||||
id := cmd.Params.Get(0).Int()
|
id := cmd.Params.Get(0).Int()
|
||||||
if id == nil {
|
if id == nil {
|
||||||
@@ -106,3 +95,83 @@ func handlerCmd(
|
|||||||
_, _ = fmt.Fprintln(os.Stderr, helpString[1:])
|
_, _ = fmt.Fprintln(os.Stderr, helpString[1:])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func applyCmdToHandler(model models.EventHandler, cmd Command) models.EventHandler {
|
||||||
|
// Remove keys
|
||||||
|
for _, elem := range cmd.Params.Strings(1) {
|
||||||
|
if elem[0] != '-' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keyToRemove := elem[1:]
|
||||||
|
|
||||||
|
if strings.HasPrefix(keyToRemove, "conditions.") {
|
||||||
|
condKeyToRemove := keyToRemove[11:]
|
||||||
|
delete(model.Conditions, condKeyToRemove)
|
||||||
|
} else if strings.HasPrefix(keyToRemove, "fire.payload.") && model.Actions.FireEvent != nil {
|
||||||
|
firePayloadKeyToRemove := keyToRemove[13:]
|
||||||
|
delete(model.Actions.FireEvent.Payload, firePayloadKeyToRemove)
|
||||||
|
} else {
|
||||||
|
switch keyToRemove {
|
||||||
|
case "set-power":
|
||||||
|
model.Actions.SetPower = nil
|
||||||
|
case "set-color":
|
||||||
|
model.Actions.SetColor = nil
|
||||||
|
case "set-intensity":
|
||||||
|
model.Actions.SetIntensity = nil
|
||||||
|
case "add-intensity":
|
||||||
|
model.Actions.AddIntensity = nil
|
||||||
|
case "fire":
|
||||||
|
fallthrough
|
||||||
|
case "fire.name":
|
||||||
|
model.Actions.FireEvent = nil
|
||||||
|
case "from":
|
||||||
|
model.From = models.Never
|
||||||
|
case "to":
|
||||||
|
model.To = models.Never
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// From / To
|
||||||
|
model.From = cmd.Params.Get("from").TimeOfDayOr(models.Never)
|
||||||
|
model.To = cmd.Params.Get("to").TimeOfDayOr(models.Never)
|
||||||
|
if model.From.IsNever() != model.To.IsNever() {
|
||||||
|
log.Fatalln("from and to must be specified together!")
|
||||||
|
}
|
||||||
|
|
||||||
|
// One shot
|
||||||
|
model.OneShot = cmd.Params.Get("one-shot").StringOr("false") == "true"
|
||||||
|
|
||||||
|
// Priority
|
||||||
|
model.Priority = cmd.Params.Get("priority").IntOr(100)
|
||||||
|
|
||||||
|
// Conditions
|
||||||
|
model.Conditions = cmd.Params.Subset("conditions").EventConditions()
|
||||||
|
|
||||||
|
// Power, Color, Intensity, Temperature
|
||||||
|
nds := cmd.Params.DeviceState("set-")
|
||||||
|
model.Actions.SetPower = nds.Power
|
||||||
|
model.Actions.SetColor = nds.Color
|
||||||
|
model.Actions.SetIntensity = nds.Intensity
|
||||||
|
|
||||||
|
// Add intensity
|
||||||
|
model.Actions.AddIntensity = cmd.Params.Get("add-intensity").Float()
|
||||||
|
|
||||||
|
// Fire
|
||||||
|
fireParams := cmd.Params.Subset("fire")
|
||||||
|
fireName := fireParams.Get("name").String()
|
||||||
|
if fireName != nil {
|
||||||
|
firePayloadParams := fireParams.Subset("payload")
|
||||||
|
|
||||||
|
model.Actions.FireEvent = &models.Event{
|
||||||
|
Name: *fireName,
|
||||||
|
Payload: make(map[string]string, len(firePayloadParams)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range firePayloadParams.StringMap() {
|
||||||
|
model.Actions.FireEvent.Payload[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,13 @@ EVENT HANDLER COMMANDS
|
|||||||
<set-power=B> <set-color=C> <set-intensity=F> <set-temperature=N> \
|
<set-power=B> <set-color=C> <set-intensity=F> <set-temperature=N> \
|
||||||
<add-intensity=F> \
|
<add-intensity=F> \
|
||||||
<fire.name=S> <fire.payload.*=S>
|
<fire.name=S> <fire.payload.*=S>
|
||||||
|
handler edit <id> \
|
||||||
|
(...all arguments from 'add', appending conditions and fire payload matchers...) \
|
||||||
|
<event=S> <search=S>
|
||||||
|
<-from> <-to> \
|
||||||
|
<-set-power> <-set-color> <-set-intensity> <-set-temperature> \
|
||||||
|
<-conditions.*> \
|
||||||
|
<-fire> <-fire.payload.*>
|
||||||
handler delete <id>
|
handler delete <id>
|
||||||
|
|
||||||
EVENT COMMANDS
|
EVENT COMMANDS
|
||||||
|
|||||||
@@ -204,9 +204,7 @@ func (b *Bridge) Run(ctx context.Context, debug bool) error {
|
|||||||
b.checkAndUpdateState(state)
|
b.checkAndUpdateState(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
if time.Since(state.discoveredTime) > time.Second*10 && time.Since(state.fwSpamTime) > time.Second * 30 {
|
if time.Since(state.discoveredTime) > time.Second*10 {
|
||||||
state.fwSpamTime = time.Now()
|
|
||||||
|
|
||||||
if state.firmware == nil {
|
if state.firmware == nil {
|
||||||
_, _ = client.Send(state.target, &GetHostFirmware{})
|
_, _ = client.Send(state.target, &GetHostFirmware{})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ type State struct {
|
|||||||
lightStateTime time.Time
|
lightStateTime time.Time
|
||||||
requestTime time.Time
|
requestTime time.Time
|
||||||
updateTime time.Time
|
updateTime time.Time
|
||||||
fwSpamTime time.Time
|
|
||||||
acksPending []uint8
|
acksPending []uint8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+90
-115
@@ -12,14 +12,13 @@ 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 {
|
||||||
@@ -118,144 +117,123 @@ func (r *DeviceRepo) FetchByReference(ctx context.Context, kind models.Reference
|
|||||||
return r.populate(ctx, records)
|
return r.populate(ctx, records)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *DeviceRepo) SaveMany(ctx context.Context, mode models.SaveMode, devices []models.Device) error {
|
func (r *DeviceRepo) Save(ctx context.Context, device *models.Device, mode models.SaveMode) 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()
|
||||||
|
|
||||||
for i, device := range devices {
|
record := deviceRecord{
|
||||||
scenesJSON, err := json.Marshal(device.SceneAssignments)
|
ID: device.ID,
|
||||||
if err != nil {
|
BridgeID: device.BridgeID,
|
||||||
return err
|
InternalID: device.InternalID,
|
||||||
}
|
Icon: device.Icon,
|
||||||
|
Name: device.Name,
|
||||||
|
Capabilities: strings.Join(models.DeviceCapabilitiesToStrings(device.Capabilities), ","),
|
||||||
|
ButtonNames: strings.Join(device.ButtonNames, ","),
|
||||||
|
}
|
||||||
|
|
||||||
record := deviceRecord{
|
if device.ID > 0 {
|
||||||
ID: device.ID,
|
_, err := tx.NamedExecContext(ctx, `
|
||||||
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,
|
||||||
name = :name,
|
name = :name,
|
||||||
capabilities = :capabilities,
|
capabilities = :capabilities,
|
||||||
button_names = :button_names,
|
button_names = :button_names
|
||||||
scene_assignments = :scene_assignments
|
|
||||||
WHERE id=:id
|
WHERE id=:id
|
||||||
`, record)
|
`, record)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Let's just be lazy for now, optimize later if need be.
|
||||||
if mode == 0 || mode&models.SMTags != 0 {
|
if mode == 0 || mode&models.SMTags != 0 {
|
||||||
for _, tag := range device.Tags {
|
_, err = tx.ExecContext(ctx, "DELETE FROM device_tag WHERE device_id=?", record.ID)
|
||||||
_, err := tx.ExecContext(ctx, "INSERT INTO device_tag (device_id, tag_name) VALUES (?, ?)", record.ID, tag)
|
if err != nil {
|
||||||
if err != nil {
|
return dbErr(err)
|
||||||
return dbErr(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if mode == 0 || mode&models.SMProperties != 0 {
|
if mode == 0 || mode&models.SMProperties != 0 {
|
||||||
for key, value := range device.UserProperties {
|
_, err = tx.ExecContext(ctx, "DELETE FROM device_property WHERE device_id=?", record.ID)
|
||||||
_, err := tx.ExecContext(ctx, "INSERT INTO device_property (device_id, prop_key, prop_value, is_user) VALUES (?, ?, ?, 1)",
|
if err != nil {
|
||||||
record.ID, key, value,
|
return dbErr(err)
|
||||||
)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} 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)
|
||||||
|
}
|
||||||
|
|
||||||
if mode == 0 || mode&models.SMState != 0 {
|
lastID, err := res.LastInsertId()
|
||||||
_, err = tx.NamedExecContext(ctx, `
|
if err != nil {
|
||||||
REPLACE INTO device_state(device_id, hue, saturation, kelvin, power, intensity)
|
return dbErr(err)
|
||||||
VALUES (:device_id, :hue, :saturation, :kelvin, :power, :intensity)
|
}
|
||||||
`, deviceStateRecord{
|
|
||||||
DeviceID: record.ID,
|
record.ID = int(lastID)
|
||||||
Hue: device.State.Color.Hue,
|
device.ID = int(lastID)
|
||||||
Saturation: device.State.Color.Saturation,
|
}
|
||||||
Kelvin: device.State.Color.Kelvin,
|
|
||||||
Power: device.State.Power,
|
if mode == 0 || mode&models.SMTags != 0 {
|
||||||
Intensity: device.State.Intensity,
|
for _, tag := range device.Tags {
|
||||||
})
|
_, err := tx.ExecContext(ctx, "INSERT INTO device_tag (device_id, tag_name) VALUES (?, ?)", record.ID, tag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return dbErr(err)
|
return dbErr(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return tx.Commit()
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (r *DeviceRepo) Save(ctx context.Context, device *models.Device, mode models.SaveMode) error {
|
for key, value := range device.DriverProperties {
|
||||||
devices := []models.Device{*device}
|
j, err := json.Marshal(value)
|
||||||
|
if err != nil {
|
||||||
|
// Eh, it'll get filled by the driver anyway
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
err := r.SaveMany(ctx, mode, devices)
|
_, err = tx.ExecContext(ctx, "INSERT INTO device_property (device_id, prop_key, prop_value, is_user) VALUES (?, ?, ?, 0)",
|
||||||
if err != nil {
|
record.ID, key, string(j),
|
||||||
return err
|
)
|
||||||
|
if err != nil {
|
||||||
|
// Return err here anyway, it might put the tx in a bad state to ignore it.
|
||||||
|
return dbErr(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
*device = devices[0]
|
if mode == 0 || mode&models.SMState != 0 {
|
||||||
return nil
|
_, 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 {
|
func (r *DeviceRepo) Delete(ctx context.Context, device *models.Device) error {
|
||||||
@@ -350,15 +328,12 @@ 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]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,118 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
+11
-82
@@ -2,24 +2,21 @@ package models
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"math"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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"`
|
||||||
SceneAssignments []DeviceSceneAssignment `json:"sceneAssignments"`
|
State DeviceState `json:"state"`
|
||||||
State DeviceState `json:"state"`
|
Tags []string `json:"tags"`
|
||||||
Tags []string `json:"tags"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeviceUpdate struct {
|
type DeviceUpdate struct {
|
||||||
@@ -40,13 +37,6 @@ type DeviceState struct {
|
|||||||
Temperature float64 `json:"temperature"`
|
Temperature float64 `json:"temperature"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeviceScene struct {
|
|
||||||
SceneID int `json:"sceneId"`
|
|
||||||
Time time.Time `json:"time"`
|
|
||||||
DurationMS int64 `json:"duration"`
|
|
||||||
Tag string `json:"tag"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type NewDeviceState struct {
|
type NewDeviceState struct {
|
||||||
Power *bool `json:"power"`
|
Power *bool `json:"power"`
|
||||||
Color *string `json:"color"`
|
Color *string `json:"color"`
|
||||||
@@ -68,7 +58,6 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,63 +178,3 @@ func (d *Device) SetState(newState NewDeviceState) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *NewDeviceState) RelativeTo(device Device) NewDeviceState {
|
|
||||||
n := NewDeviceState{}
|
|
||||||
|
|
||||||
if s.Intensity != nil {
|
|
||||||
intensity := device.State.Intensity * *s.Intensity
|
|
||||||
n.Intensity = &intensity
|
|
||||||
}
|
|
||||||
if s.Color != nil {
|
|
||||||
c, err := ParseColorValue(*s.Color)
|
|
||||||
if err == nil {
|
|
||||||
c.Hue = math.Mod(device.State.Color.Hue + c.Hue, 360)
|
|
||||||
c.Saturation *= device.State.Color.Saturation
|
|
||||||
c.Kelvin += device.State.Color.Kelvin
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NewDeviceState) Interpolate(other NewDeviceState, fac float64) NewDeviceState {
|
|
||||||
n := NewDeviceState{}
|
|
||||||
|
|
||||||
if s.Power != nil && other.Power != nil {
|
|
||||||
if fac >= 0.5 {
|
|
||||||
n.Power = other.Power
|
|
||||||
} else {
|
|
||||||
n.Power = s.Power
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.Color != nil && other.Color != nil {
|
|
||||||
sc, err := ParseColorValue(*s.Color)
|
|
||||||
oc, err2 := ParseColorValue(*other.Color)
|
|
||||||
if err == nil && err2 == nil {
|
|
||||||
rc := ColorValue{}
|
|
||||||
rc.Hue = interpolateFloat(sc.Hue, oc.Hue, fac)
|
|
||||||
rc.Saturation = interpolateFloat(sc.Saturation, oc.Saturation, fac)
|
|
||||||
rc.Kelvin = interpolateInt(sc.Kelvin, oc.Kelvin, fac)
|
|
||||||
|
|
||||||
rcStr := rc.String()
|
|
||||||
n.Color = &rcStr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.Intensity != nil && other.Intensity != nil {
|
|
||||||
n.Intensity = new(float64)
|
|
||||||
*n.Intensity = interpolateFloat(*s.Intensity, *other.Intensity, fac)
|
|
||||||
}
|
|
||||||
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
func interpolateFloat(a, b, fac float64) float64 {
|
|
||||||
return (a * (1 - fac)) + (b * fac)
|
|
||||||
}
|
|
||||||
|
|
||||||
func interpolateInt(a, b int, fac float64) int {
|
|
||||||
return int((float64(a) * (1 - fac)) + (float64(b) * fac))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -22,10 +22,3 @@ var ErrInvalidPacketSize = errors.New("invalid packet size")
|
|||||||
var ErrReadTimeout = errors.New("read timeout")
|
var ErrReadTimeout = errors.New("read timeout")
|
||||||
var ErrUnrecognizedPacketType = errors.New("packet type not recognized")
|
var ErrUnrecognizedPacketType = errors.New("packet type not recognized")
|
||||||
var ErrBridgeRunningRequired = errors.New("this operation cannot be performed when bridge is not running")
|
var ErrBridgeRunningRequired = errors.New("this operation cannot be performed when bridge is not running")
|
||||||
|
|
||||||
var ErrSceneInvalidInterval = errors.New("scene interval must be 0 (=disabled) or greater")
|
|
||||||
var ErrSceneNoRoles = errors.New("scene cannot have zero rules")
|
|
||||||
var ErrSceneRoleNoStates = errors.New("scene rule has no states")
|
|
||||||
var ErrSceneRoleUnsupportedOrdering = errors.New("scene rule has an unsupported ordering")
|
|
||||||
var ErrSceneRoleUnknownEffect = errors.New("scene rule has an unknown effect")
|
|
||||||
var ErrSceneRoleUnknownPowerMode = errors.New("scene rule has an unknown power mode")
|
|
||||||
|
|||||||
-247
@@ -1,247 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"math"
|
|
||||||
"math/rand"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Scene struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
IntervalMS int64 `json:"interval"`
|
|
||||||
Roles []SceneRole `json:"roles"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Scene) Validate() error {
|
|
||||||
if s.IntervalMS < 0 {
|
|
||||||
return ErrSceneInvalidInterval
|
|
||||||
}
|
|
||||||
if len(s.Roles) == 0 {
|
|
||||||
return ErrSceneNoRoles
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, role := range s.Roles {
|
|
||||||
err := role.Validate()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Scene) Role(device *Device) *SceneRole {
|
|
||||||
index := s.RoleIndex(device)
|
|
||||||
if index == -1 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &s.Roles[index]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Scene) RoleIndex(device *Device) int {
|
|
||||||
for i, role := range s.Roles {
|
|
||||||
if role.TargetKind.Matches(device, role.TargetValue) {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
type SceneEffect string
|
|
||||||
|
|
||||||
const (
|
|
||||||
SEStatic SceneEffect = "Plain"
|
|
||||||
SERandom SceneEffect = "Random"
|
|
||||||
SEGradient SceneEffect = "Gradient"
|
|
||||||
SEWalkingGradient SceneEffect = "WalkingGradient"
|
|
||||||
SETransition SceneEffect = "Transition"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SceneRole struct {
|
|
||||||
Effect SceneEffect `json:"effect"`
|
|
||||||
PowerMode ScenePowerMode `json:"powerMode"`
|
|
||||||
TargetKind ReferenceKind `json:"targetKind"`
|
|
||||||
TargetValue string `json:"targetValue"`
|
|
||||||
Interpolate bool `json:"interpolate"`
|
|
||||||
Relative bool `json:"relative"`
|
|
||||||
Order string `json:"order"`
|
|
||||||
States []NewDeviceState `json:"states"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SceneRunContext struct {
|
|
||||||
Index int
|
|
||||||
Length int
|
|
||||||
IntervalNumber int64
|
|
||||||
IntervalMax int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *SceneRunContext) PositionFacShifted() float64 {
|
|
||||||
shiftFac := float64(d.IntervalNumber%int64(d.Length)) / 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 {
|
|
||||||
if len(r.States) == 0 {
|
|
||||||
return ErrSceneRoleNoStates
|
|
||||||
}
|
|
||||||
|
|
||||||
switch r.TargetKind {
|
|
||||||
case RKTag, RKBridgeID, RKDeviceID, RKName, RKAll:
|
|
||||||
default:
|
|
||||||
return ErrBadInput
|
|
||||||
}
|
|
||||||
|
|
||||||
switch r.PowerMode {
|
|
||||||
case SPScene, SPDevice:
|
|
||||||
default:
|
|
||||||
return ErrSceneRoleUnknownPowerMode
|
|
||||||
}
|
|
||||||
|
|
||||||
switch r.Effect {
|
|
||||||
case SEStatic, SERandom, SEGradient, SETransition:
|
|
||||||
default:
|
|
||||||
return ErrSceneRoleUnknownEffect
|
|
||||||
}
|
|
||||||
|
|
||||||
switch r.Order {
|
|
||||||
case "", "-name", "name", "+name", "-id", "id", "+id":
|
|
||||||
default:
|
|
||||||
return ErrSceneRoleUnsupportedOrdering
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *SceneRole) ApplyOrder(devices []Device) {
|
|
||||||
if r.Order == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
desc := strings.HasPrefix(r.Order, "-")
|
|
||||||
|
|
||||||
orderKey := r.Order
|
|
||||||
if desc || strings.HasPrefix(r.Order, "+") {
|
|
||||||
orderKey = orderKey[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
switch orderKey {
|
|
||||||
case "id":
|
|
||||||
sort.Slice(devices, func(i, j int) bool {
|
|
||||||
if desc {
|
|
||||||
return devices[i].ID > devices[j].ID
|
|
||||||
}
|
|
||||||
|
|
||||||
return devices[i].ID < devices[j].ID
|
|
||||||
})
|
|
||||||
case "name":
|
|
||||||
sort.Slice(devices, func(i, j int) bool {
|
|
||||||
if desc {
|
|
||||||
return strings.Compare(devices[i].Name, devices[j].Name) < 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Compare(devices[i].Name, devices[j].Name) > 0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *SceneRole) ApplyEffect(device *Device, c SceneRunContext) (newState NewDeviceState) {
|
|
||||||
switch r.Effect {
|
|
||||||
case SEStatic:
|
|
||||||
newState = r.States[0]
|
|
||||||
case SERandom:
|
|
||||||
newState = r.State(rand.Float64())
|
|
||||||
case SEGradient:
|
|
||||||
newState = r.State(c.PositionFac())
|
|
||||||
case SEWalkingGradient:
|
|
||||||
newState = r.State(c.PositionFacShifted())
|
|
||||||
case SETransition:
|
|
||||||
newState = r.State(c.IntervalFac())
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Relative {
|
|
||||||
newState = newState.RelativeTo(*device)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch r.PowerMode {
|
|
||||||
case SPDevice:
|
|
||||||
newState.Power = nil
|
|
||||||
case SPScene:
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *SceneRole) State(fac float64) NewDeviceState {
|
|
||||||
if len(r.States) == 0 {
|
|
||||||
return NewDeviceState{}
|
|
||||||
} else if len(r.States) == 1 {
|
|
||||||
return r.States[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if r.Interpolate {
|
|
||||||
if len(r.States) == 2 {
|
|
||||||
return r.States[0].Interpolate(r.States[1], fac)
|
|
||||||
} else {
|
|
||||||
pos := fac * float64(len(r.States)-1)
|
|
||||||
start := math.Floor(pos)
|
|
||||||
localFac := pos - start
|
|
||||||
|
|
||||||
return r.States[int(start)].Interpolate(r.States[int(start)+1], localFac)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
index := int(fac * float64(len(r.States)))
|
|
||||||
if index == len(r.States) {
|
|
||||||
index = len(r.States) - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.States[index]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ScenePowerMode string
|
|
||||||
|
|
||||||
const (
|
|
||||||
SPDevice ScenePowerMode = "Device" // Device state decides power. Scene state may only power off.
|
|
||||||
SPScene ScenePowerMode = "Scene" // Scene state decide power.
|
|
||||||
)
|
|
||||||
|
|
||||||
// DeviceSceneAssignment is an entry on the device stack. StartTime and DurationMS are only respected when the scene
|
|
||||||
// instance is created for the group.
|
|
||||||
type DeviceSceneAssignment struct {
|
|
||||||
SceneID int `json:"sceneId"`
|
|
||||||
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
|
|
||||||
}
|
|
||||||
+1
-51
@@ -15,56 +15,6 @@ var (
|
|||||||
RKName ReferenceKind = "Name"
|
RKName ReferenceKind = "Name"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (rk ReferenceKind) Matches(device *Device, value string) bool {
|
|
||||||
switch rk {
|
|
||||||
case RKName:
|
|
||||||
if strings.HasPrefix(value, "*") {
|
|
||||||
return strings.HasSuffix(device.Name, value[1:])
|
|
||||||
} else if strings.HasSuffix(value, "*") {
|
|
||||||
return strings.HasPrefix(device.Name, value[:len(value)-1])
|
|
||||||
} else {
|
|
||||||
return device.Name == value
|
|
||||||
}
|
|
||||||
case RKDeviceID:
|
|
||||||
idStr := strconv.Itoa(device.ID)
|
|
||||||
for _, idStr2 := range strings.Split(value, ",") {
|
|
||||||
if idStr == idStr2 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case RKBridgeID:
|
|
||||||
idStr := strconv.Itoa(device.BridgeID)
|
|
||||||
for _, idStr2 := range strings.Split(value, ",") {
|
|
||||||
if idStr == idStr2 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case RKTag:
|
|
||||||
hadAny := false
|
|
||||||
for _, tag := range strings.Split(value, ",") {
|
|
||||||
if strings.HasPrefix(tag, "-") {
|
|
||||||
if device.HasTag(tag[1:]) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else if strings.HasPrefix(tag, "+") {
|
|
||||||
if !device.HasTag(tag[1:]) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if device.HasTag(tag) {
|
|
||||||
hadAny = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return hadAny
|
|
||||||
case RKAll:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseFetchString(fetchStr string) (ReferenceKind, string) {
|
func ParseFetchString(fetchStr string) (ReferenceKind, string) {
|
||||||
if strings.HasPrefix(fetchStr, "tag:") {
|
if strings.HasPrefix(fetchStr, "tag:") {
|
||||||
return RKTag, fetchStr[4:]
|
return RKTag, fetchStr[4:]
|
||||||
@@ -84,4 +34,4 @@ func ParseFetchString(fetchStr string) (ReferenceKind, string) {
|
|||||||
|
|
||||||
return RKDeviceID, fetchStr
|
return RKDeviceID, fetchStr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
-- +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
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
-- +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