new scene system is kind of working. I haven't tested changing between scenes and Transition effect yet, though.
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
+7
-14
@@ -3,7 +3,6 @@ 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"
|
"golang.org/x/sync/errgroup"
|
||||||
@@ -56,14 +55,12 @@ 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 <- scene.GlobalManager().FilterUnassigned(changed)
|
config.PublishChannel <- changed
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for _, device := range changed {
|
for _, device := range changed {
|
||||||
@@ -126,11 +123,9 @@ 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 <- scene.GlobalManager().FilterUnassigned(devices)
|
config.PublishChannel <- devices
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for _, device := range devices {
|
for _, device := range devices {
|
||||||
@@ -217,7 +212,7 @@ func Devices(r gin.IRoutes) {
|
|||||||
return []models.Device{}, nil
|
return []models.Device{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
assignedScene, err := config.SceneRepository().Find(ctxOf(c), body.SceneID)
|
_, err = config.SceneRepository().Find(ctxOf(c), body.SceneID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -228,17 +223,15 @@ func Devices(r gin.IRoutes) {
|
|||||||
|
|
||||||
for i := range devices {
|
for i := range devices {
|
||||||
devices[i].SceneAssignments = []models.DeviceSceneAssignment{body}
|
devices[i].SceneAssignments = []models.DeviceSceneAssignment{body}
|
||||||
|
|
||||||
err := scene.GlobalManager().UpdateDevice(ctxOf(c), &devices[i], assignedScene)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
config.PublishChannel <- devices
|
||||||
|
|
||||||
eg := errgroup.Group{}
|
eg := errgroup.Group{}
|
||||||
for i := range devices {
|
for i := range devices {
|
||||||
|
device := devices[i]
|
||||||
|
|
||||||
eg.Go(func() error {
|
eg.Go(func() error {
|
||||||
return config.DeviceRepository().Save(ctxOf(c), &devices[i], 0)
|
return config.DeviceRepository().Save(ctxOf(c), &device, 0)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-4
@@ -6,19 +6,26 @@ import (
|
|||||||
"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"
|
"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()
|
||||||
|
|
||||||
go scene.GlobalManager().Run(context.Background())
|
|
||||||
|
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
ginny := gin.New()
|
ginny := gin.New()
|
||||||
|
|
||||||
|
|||||||
+1
-11
@@ -4,7 +4,6 @@ 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"
|
||||||
@@ -158,7 +157,7 @@ func handleEvent(event models.Event) (responses []models.Event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config.PublishChannel <- scene.GlobalManager().FilterUnassigned(allDevices)
|
config.PublishChannel <- allDevices
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
|
|
||||||
@@ -172,15 +171,6 @@ func handleEvent(event models.Event) (responses []models.Event) {
|
|||||||
|
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
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 {
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
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")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if bridge.Driver == models.DTLIFX {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -10,32 +10,34 @@ type Scene struct {
|
|||||||
group string
|
group string
|
||||||
startTime time.Time
|
startTime time.Time
|
||||||
endTime time.Time
|
endTime time.Time
|
||||||
interval time.Duration
|
|
||||||
roleMap map[int]int
|
roleMap map[int]int
|
||||||
roleList map[int][]models.Device
|
roleList map[int][]models.Device
|
||||||
|
|
||||||
due bool
|
due bool
|
||||||
|
lastInterval int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateScene updates the scene data and re-seats all devices.
|
// UpdateScene updates the scene data and re-seats all devices.
|
||||||
func (s *Scene) UpdateScene(data models.Scene) {
|
func (s *Scene) UpdateScene(data models.Scene) {
|
||||||
devices := make([]models.Device, 0, 16)
|
devices := make([]models.Device, 0, 16)
|
||||||
|
|
||||||
for ri, list := range s.roleList {
|
// Collect all devices into the undefined role (-1)
|
||||||
|
for _, list := range s.roleList {
|
||||||
for _, device := range list {
|
for _, device := range list {
|
||||||
devices = append(devices, device)
|
devices = append(devices, device)
|
||||||
s.roleMap[device.ID] = -1
|
s.roleMap[device.ID] = -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
s.roleList = map[int][]models.Device{-1: devices}
|
||||||
|
|
||||||
s.roleList = map[int][]models.Device {-1: devices}
|
// Update data and reset devices.
|
||||||
|
s.data = &data
|
||||||
for _, device := range append(devices[:0:0], devices...) {
|
for _, device := range append(devices[:0:0], devices...) {
|
||||||
s.UpsertDevice(device)
|
s.UpsertDevice(device)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpsertDevice moves the device if neccesary and updates its state.
|
// UpsertDevice moves the device if necessary and updates its state.
|
||||||
func (s *Scene) UpsertDevice(device models.Device) {
|
func (s *Scene) UpsertDevice(device models.Device) {
|
||||||
if s.data == nil {
|
if s.data == nil {
|
||||||
s.roleMap[device.ID] = -1
|
s.roleMap[device.ID] = -1
|
||||||
@@ -46,29 +48,23 @@ func (s *Scene) UpsertDevice(device models.Device) {
|
|||||||
oldIndex, hasOldIndex := s.roleMap[device.ID]
|
oldIndex, hasOldIndex := s.roleMap[device.ID]
|
||||||
newIndex := s.data.RoleIndex(&device)
|
newIndex := s.data.RoleIndex(&device)
|
||||||
|
|
||||||
if hasOldIndex {
|
s.roleMap[device.ID] = newIndex
|
||||||
if oldIndex == newIndex {
|
|
||||||
for i, device2 := range s.roleList[newIndex] {
|
|
||||||
if device2.ID == device.ID {
|
|
||||||
s.roleList[newIndex][i] = device
|
|
||||||
s.data.Roles[oldIndex].ApplyOrder(s.roleList[oldIndex])
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
if hasOldIndex {
|
||||||
} else {
|
for i, device2 := range s.roleList[oldIndex] {
|
||||||
for i, device2 := range s.roleList[oldIndex] {
|
if device2.ID == device.ID {
|
||||||
if device2.ID == device.ID {
|
s.roleList[oldIndex] = append(s.roleList[oldIndex][:i], s.roleList[oldIndex][i+1:]...)
|
||||||
s.roleList[oldIndex] = append(s.roleList[oldIndex][:i], s.roleList[oldIndex][i+1:]...)
|
break
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.due = true
|
||||||
|
|
||||||
s.roleList[newIndex] = append(s.roleList[newIndex], device)
|
s.roleList[newIndex] = append(s.roleList[newIndex], device)
|
||||||
s.data.Roles[newIndex].ApplyOrder(s.roleList[newIndex])
|
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.
|
// RemoveDevice finds and remove a device. It's a noop if the device does not exist in this scene.
|
||||||
@@ -85,9 +81,37 @@ func (s *Scene) RemoveDevice(device models.Device) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.due = true
|
||||||
|
|
||||||
delete(s.roleMap, device.ID)
|
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
|
// Run runs the scene
|
||||||
func (s *Scene) Run() []models.Device {
|
func (s *Scene) Run() []models.Device {
|
||||||
if s.data == nil {
|
if s.data == nil {
|
||||||
@@ -96,11 +120,12 @@ func (s *Scene) Run() []models.Device {
|
|||||||
|
|
||||||
intervalNumber := int64(0)
|
intervalNumber := int64(0)
|
||||||
intervalMax := int64(1)
|
intervalMax := int64(1)
|
||||||
if s.interval > 0 {
|
if s.data.IntervalMS > 0 {
|
||||||
intervalNumber = int64(time.Since(s.startTime) / s.interval)
|
interval := time.Duration(s.data.IntervalMS) * time.Millisecond
|
||||||
|
intervalNumber = int64(time.Since(s.startTime) / interval)
|
||||||
|
|
||||||
if !s.endTime.IsZero() {
|
if !s.endTime.IsZero() {
|
||||||
intervalMax = int64(s.endTime.Sub(s.startTime) / s.interval)
|
intervalMax = int64(s.endTime.Sub(s.startTime) / interval)
|
||||||
} else {
|
} else {
|
||||||
intervalMax = intervalNumber + 1
|
intervalMax = intervalNumber + 1
|
||||||
}
|
}
|
||||||
@@ -131,5 +156,8 @@ func (s *Scene) Run() []models.Device {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.due = false
|
||||||
|
s.lastInterval = intervalNumber
|
||||||
|
|
||||||
return updatedDevices
|
return updatedDevices
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,7 +149,8 @@ func (r *DeviceRepo) SaveMany(ctx context.Context, mode models.SaveMode, devices
|
|||||||
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 {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package models
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -189,6 +190,25 @@ 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 {
|
func (s *NewDeviceState) Interpolate(other NewDeviceState, fac float64) NewDeviceState {
|
||||||
n := NewDeviceState{}
|
n := NewDeviceState{}
|
||||||
|
|
||||||
|
|||||||
+10
-3
@@ -69,6 +69,7 @@ type SceneRole struct {
|
|||||||
TargetKind ReferenceKind `json:"targetKind"`
|
TargetKind ReferenceKind `json:"targetKind"`
|
||||||
TargetValue string `json:"targetValue"`
|
TargetValue string `json:"targetValue"`
|
||||||
Interpolate bool `json:"interpolate"`
|
Interpolate bool `json:"interpolate"`
|
||||||
|
Relative bool `json:"relative"`
|
||||||
Order string `json:"order"`
|
Order string `json:"order"`
|
||||||
States []NewDeviceState `json:"states"`
|
States []NewDeviceState `json:"states"`
|
||||||
}
|
}
|
||||||
@@ -81,7 +82,7 @@ type SceneRunContext struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *SceneRunContext) PositionFacShifted() float64 {
|
func (d *SceneRunContext) PositionFacShifted() float64 {
|
||||||
shiftFac := float64((int64(d.Index)+d.IntervalNumber)%int64(d.Length)) * (1 / float64(d.Length))
|
shiftFac := float64(d.IntervalNumber%int64(d.Length)) / float64(d.Length)
|
||||||
|
|
||||||
return math.Mod(d.PositionFac()+shiftFac, 1)
|
return math.Mod(d.PositionFac()+shiftFac, 1)
|
||||||
}
|
}
|
||||||
@@ -179,6 +180,10 @@ func (r *SceneRole) ApplyEffect(device *Device, c SceneRunContext) (newState New
|
|||||||
newState = r.State(c.IntervalFac())
|
newState = r.State(c.IntervalFac())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if r.Relative {
|
||||||
|
newState = newState.RelativeTo(*device)
|
||||||
|
}
|
||||||
|
|
||||||
switch r.PowerMode {
|
switch r.PowerMode {
|
||||||
case SPDevice:
|
case SPDevice:
|
||||||
newState.Power = nil
|
newState.Power = nil
|
||||||
@@ -196,6 +201,8 @@ func (r *SceneRole) State(fac float64) NewDeviceState {
|
|||||||
return r.States[0]
|
return r.States[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if r.Interpolate {
|
if r.Interpolate {
|
||||||
if len(r.States) == 2 {
|
if len(r.States) == 2 {
|
||||||
return r.States[0].Interpolate(r.States[1], fac)
|
return r.States[0].Interpolate(r.States[1], fac)
|
||||||
@@ -219,8 +226,8 @@ func (r *SceneRole) State(fac float64) NewDeviceState {
|
|||||||
type ScenePowerMode string
|
type ScenePowerMode string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SPDevice ScenePowerMode = "Device" // Device state decides power. Scene state may only power off.
|
SPDevice ScenePowerMode = "Device" // Device state decides power. Scene state may only power off.
|
||||||
SPScene ScenePowerMode = "Scene" // Scene state decide power.
|
SPScene ScenePowerMode = "Scene" // Scene state decide power.
|
||||||
)
|
)
|
||||||
|
|
||||||
// DeviceSceneAssignment is an entry on the device stack. StartTime and DurationMS are only respected when the scene
|
// DeviceSceneAssignment is an entry on the device stack. StartTime and DurationMS are only respected when the scene
|
||||||
|
|||||||
Reference in New Issue
Block a user