You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

207 lines
4.3 KiB

package services
import (
lucifer3 "git.aiterp.net/lucifer3/server"
"git.aiterp.net/lucifer3/server/commands"
"git.aiterp.net/lucifer3/server/device"
"git.aiterp.net/lucifer3/server/events"
"git.aiterp.net/lucifer3/server/internal/gentools"
"github.com/google/uuid"
"sync"
"sync/atomic"
"time"
)
func NewEffectEnforcer(resolver device.Resolver, sceneMap device.SceneMap) lucifer3.ActiveService {
s := &effectEnforcer{
resolver: resolver,
sceneMap: sceneMap,
list: make([]*effectEnforcerRun, 0, 16),
index: make(map[string]*effectEnforcerRun, 8),
}
return s
}
type effectEnforcer struct {
mu sync.Mutex
resolver device.Resolver
sceneMap device.SceneMap
started uint32
list []*effectEnforcerRun
index map[string]*effectEnforcerRun
}
func (s *effectEnforcer) Active() bool {
return true
}
func (s *effectEnforcer) HandleEvent(_ *lucifer3.EventBus, event lucifer3.Event) {
switch event := event.(type) {
case events.DeviceReady:
// If the device is managed by the effect enforcer, cause the effect to be
// re-ran.
s.mu.Lock()
if run, ok := s.index[event.ID]; ok {
run.due = time.Now()
}
s.mu.Unlock()
}
}
func (s *effectEnforcer) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Command) {
switch command := command.(type) {
case commands.Assign:
pointers := s.resolver.Resolve(command.Match)
allowedIDs := make([]string, 0, len(pointers))
for _, ptr := range pointers {
if s.sceneMap.SceneID(ptr.ID) == nil {
allowedIDs = append(allowedIDs, ptr.ID)
}
}
if len(pointers) == 0 {
if command.ID != nil {
bus.RunEvent(events.AssignmentRemoved{ID: *command.ID})
} else {
bus.RunEvent(events.Log{
Level: "info",
Code: "assignment_matched_none",
Message: "Assignment to \"" + command.Match + "\" matched no devices.",
})
}
return
}
id := uuid.New()
if command.ID != nil {
id = *command.ID
}
bus.RunEvent(events.AssignmentCreated{
ID: id,
Match: command.Match,
Effect: command.Effect,
})
s.mu.Lock()
// Create a new run
newRun := &effectEnforcerRun{
id: id,
due: time.Now(),
ids: allowedIDs,
effect: command.Effect,
}
s.list = append(s.list, newRun)
// Switch over the indices.
for _, id := range allowedIDs {
if oldRun := s.index[id]; oldRun != nil {
oldRun.remove(id)
}
s.index[id] = newRun
}
s.mu.Unlock()
if atomic.CompareAndSwapUint32(&s.started, 0, 1) {
go s.runLoop(bus)
}
for _, deviceID := range allowedIDs {
bus.RunEvent(events.DeviceAssigned{
DeviceID: deviceID,
AssignmentID: gentools.Ptr(id),
})
}
}
}
func (s *effectEnforcer) runLoop(bus *lucifer3.EventBus) {
deleteList := make([]int, 0, 8)
batch := make(commands.SetStateBatch, 64)
deleteIDs := make([]uuid.UUID, 0)
for now := range time.NewTicker(time.Millisecond * 50).C {
deleteIDs = deleteIDs[:0]
s.mu.Lock()
for i, run := range s.list {
if run.dead {
deleteIDs = append(deleteIDs, run.id)
deleteList = append(deleteList, i-len(deleteList))
continue
}
if run.due.IsZero() || now.Before(run.due) {
continue
}
for j, id := range run.ids {
if id == "" {
continue
}
state := run.effect.State(j, len(run.ids), run.round)
batch[id] = state
}
if freq := run.effect.Frequency(); freq > 0 {
if freq < 100*time.Millisecond {
run.due = now
} else {
run.due = run.due.Add(freq)
}
run.round += 1
} else {
run.due = time.Time{}
}
}
if len(deleteList) > 0 {
for _, i := range deleteList {
s.list = append(s.list[:i], s.list[i+1:]...)
}
deleteList = deleteList[:0]
}
s.mu.Unlock()
for _, id := range deleteIDs {
bus.RunEvent(events.AssignmentRemoved{ID: id})
}
if len(batch) > 0 {
bus.RunCommand(batch)
batch = make(commands.SetStateBatch, 64)
}
}
}
type effectEnforcerRun struct {
id uuid.UUID
due time.Time
ids []string
effect lucifer3.Effect
dead bool
round int
}
// remove takes out an id from the effect, and returns whether the effect is empty.
func (r *effectEnforcerRun) remove(id string) {
anyLeft := false
for i, id2 := range r.ids {
if id2 == id {
r.ids[i] = ""
} else if id2 != "" {
anyLeft = true
}
}
if !anyLeft {
r.dead = true
}
return
}