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:
@@ -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
|
||||
startTime time.Time
|
||||
endTime time.Time
|
||||
interval time.Duration
|
||||
roleMap map[int]int
|
||||
roleList map[int][]models.Device
|
||||
|
||||
due bool
|
||||
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)
|
||||
|
||||
for ri, list := range s.roleList {
|
||||
// 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}
|
||||
|
||||
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 neccesary and updates its state.
|
||||
// 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
|
||||
@@ -46,29 +48,23 @@ func (s *Scene) UpsertDevice(device models.Device) {
|
||||
oldIndex, hasOldIndex := s.roleMap[device.ID]
|
||||
newIndex := s.data.RoleIndex(&device)
|
||||
|
||||
if hasOldIndex {
|
||||
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
|
||||
}
|
||||
}
|
||||
s.roleMap[device.ID] = newIndex
|
||||
|
||||
return
|
||||
} else {
|
||||
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
|
||||
}
|
||||
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)
|
||||
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.
|
||||
@@ -85,9 +81,37 @@ func (s *Scene) RemoveDevice(device models.Device) {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -96,11 +120,12 @@ func (s *Scene) Run() []models.Device {
|
||||
|
||||
intervalNumber := int64(0)
|
||||
intervalMax := int64(1)
|
||||
if s.interval > 0 {
|
||||
intervalNumber = int64(time.Since(s.startTime) / s.interval)
|
||||
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) / s.interval)
|
||||
intervalMax = int64(s.endTime.Sub(s.startTime) / interval)
|
||||
} else {
|
||||
intervalMax = intervalNumber + 1
|
||||
}
|
||||
@@ -131,5 +156,8 @@ func (s *Scene) Run() []models.Device {
|
||||
}
|
||||
}
|
||||
|
||||
s.due = false
|
||||
s.lastInterval = intervalNumber
|
||||
|
||||
return updatedDevices
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user