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.
372 lines
8.5 KiB
372 lines
8.5 KiB
package effectenforcer
|
|
|
|
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/color"
|
|
"git.aiterp.net/lucifer3/server/internal/gentools"
|
|
"github.com/google/uuid"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
func NewService(resolver device.Resolver) lucifer3.ActiveService {
|
|
s := &effectEnforcer{
|
|
resolver: resolver,
|
|
list: make([]*effectEnforcerRun, 0, 16),
|
|
index: make(map[string]*effectEnforcerRun, 8),
|
|
|
|
supportFlags: make(map[string]device.SupportFlags),
|
|
colorFlags: make(map[string]device.ColorFlags),
|
|
temperatures: make(map[string]float64),
|
|
motions: make(map[string]float64),
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
type effectEnforcer struct {
|
|
mu sync.Mutex
|
|
resolver device.Resolver
|
|
|
|
supportFlags map[string]device.SupportFlags
|
|
colorFlags map[string]device.ColorFlags
|
|
temperatures map[string]float64
|
|
motions map[string]float64
|
|
|
|
started uint32
|
|
|
|
list []*effectEnforcerRun
|
|
index map[string]*effectEnforcerRun
|
|
}
|
|
|
|
func (s *effectEnforcer) Active() bool {
|
|
return true
|
|
}
|
|
|
|
func (s *effectEnforcer) HandleEvent(bus *lucifer3.EventBus, event lucifer3.Event) {
|
|
switch event := event.(type) {
|
|
case events.HardwareState:
|
|
s.mu.Lock()
|
|
colorFlags := s.colorFlags
|
|
supportFlags := s.supportFlags
|
|
s.mu.Unlock()
|
|
|
|
colorFlags = gentools.CopyMap(colorFlags)
|
|
supportFlags = gentools.CopyMap(supportFlags)
|
|
colorFlags[event.ID] = event.ColorFlags
|
|
supportFlags[event.ID] = event.SupportFlags
|
|
|
|
s.mu.Lock()
|
|
s.colorFlags = colorFlags
|
|
s.supportFlags = supportFlags
|
|
s.mu.Unlock()
|
|
|
|
case events.DeviceReady:
|
|
// If the device is managed by the effect enforcer, cause the effect to run again
|
|
s.mu.Lock()
|
|
if run, ok := s.index[event.ID]; ok {
|
|
run.due = time.Now()
|
|
}
|
|
s.mu.Unlock()
|
|
|
|
case events.DeviceAssigned:
|
|
s.triggerVariableEffects(bus, event.DeviceID)
|
|
|
|
case events.MotionSensed:
|
|
s.mu.Lock()
|
|
s.motions[event.ID] = event.SecondsSince
|
|
s.mu.Unlock()
|
|
|
|
s.triggerVariableEffects(bus, event.ID)
|
|
case events.TemperatureChanged:
|
|
s.mu.Lock()
|
|
s.temperatures[event.ID] = event.Temperature
|
|
s.mu.Unlock()
|
|
|
|
s.triggerVariableEffects(bus, event.ID)
|
|
}
|
|
}
|
|
|
|
func (s *effectEnforcer) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Command) {
|
|
switch command := command.(type) {
|
|
case commands.Assign:
|
|
pointers := s.resolver.Resolve(command.Match)
|
|
deviceIDs := make([]string, 0, len(pointers))
|
|
for _, ptr := range pointers {
|
|
deviceIDs = append(deviceIDs, 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{
|
|
match: command.Match,
|
|
id: id,
|
|
due: time.Now(),
|
|
ids: deviceIDs,
|
|
effect: command.Effect,
|
|
}
|
|
s.list = append(s.list, newRun)
|
|
|
|
// Switch over the indices.
|
|
for _, id := range deviceIDs {
|
|
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 deviceIDs {
|
|
bus.RunEvent(events.DeviceAssigned{
|
|
DeviceID: deviceID,
|
|
AssignmentID: gentools.Ptr(id),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *effectEnforcer) triggerVariableEffects(bus *lucifer3.EventBus, id string) {
|
|
now := time.Now()
|
|
|
|
s.mu.Lock()
|
|
for _, run := range s.list {
|
|
found := false
|
|
for _, id2 := range run.ids {
|
|
if id2 == id {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
continue
|
|
}
|
|
|
|
run.variables = map[string]float64{}
|
|
|
|
totalMotion := 0.0
|
|
motionSamples := 0
|
|
minMotion := 0.0
|
|
maxMotion := 0.0
|
|
totalTemperature := 0.0
|
|
temperatureSamples := 0
|
|
minTemperature := 0.0
|
|
maxTemperature := 0.0
|
|
for _, id := range run.ids {
|
|
if motion, ok := s.motions[id]; ok {
|
|
totalMotion += motion
|
|
motionSamples += 1
|
|
if motion < minMotion || motionSamples == 1 {
|
|
minMotion = motion
|
|
}
|
|
if motion > maxMotion {
|
|
maxMotion = motion
|
|
}
|
|
}
|
|
if temperature, ok := s.temperatures[id]; ok {
|
|
totalTemperature += temperature
|
|
temperatureSamples += 1
|
|
if temperature < minTemperature || temperatureSamples == 1 {
|
|
minTemperature = temperature
|
|
}
|
|
if temperature > maxTemperature {
|
|
maxTemperature = temperature
|
|
}
|
|
}
|
|
}
|
|
|
|
if temperatureSamples > 0 {
|
|
run.variables["temperature.avg"] = totalTemperature / float64(temperatureSamples)
|
|
run.variables["temperature.min"] = minTemperature
|
|
run.variables["temperature.max"] = maxTemperature
|
|
}
|
|
if motionSamples > 0 {
|
|
run.variables["motion.avg"] = totalMotion / float64(motionSamples)
|
|
run.variables["motion.min"] = minMotion
|
|
run.variables["motion.max"] = maxMotion
|
|
}
|
|
|
|
bus.RunEvent(events.AssignmentVariables{ID: run.id, Map: gentools.CopyMap(run.variables)})
|
|
|
|
if _, ok := run.effect.(lucifer3.VariableEffect); ok {
|
|
run.due = now
|
|
}
|
|
}
|
|
s.mu.Unlock()
|
|
}
|
|
|
|
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()
|
|
colorFlags := s.colorFlags
|
|
supportFlags := s.supportFlags
|
|
|
|
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)
|
|
if vEff, ok := run.effect.(lucifer3.VariableEffect); ok {
|
|
variableName := vEff.VariableName()
|
|
if variable, ok := run.variables[variableName]; ok {
|
|
state = vEff.VariableState(j, len(run.ids), variable)
|
|
}
|
|
}
|
|
|
|
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 {
|
|
for id, state := range batch {
|
|
sf := supportFlags[id]
|
|
|
|
if state.Power != nil && !sf.HasAny(device.SFlagPower) {
|
|
state.Power = nil
|
|
}
|
|
if state.Temperature != nil && !sf.HasAny(device.SFlagTemperature) {
|
|
state.Temperature = nil
|
|
}
|
|
if state.Intensity != nil && !sf.HasAny(device.SFlagIntensity) {
|
|
state.Intensity = nil
|
|
}
|
|
if state.Color != nil && !sf.HasAny(device.SFlagColor) {
|
|
state.Color = nil
|
|
} else if state.Color != nil {
|
|
cf := colorFlags[id]
|
|
invalid := (state.Color.K != nil && !cf.HasAll(device.CFlagKelvin)) ||
|
|
(state.Color.XY != nil && !cf.HasAll(device.CFlagXY)) ||
|
|
(state.Color.RGB != nil && !cf.HasAll(device.CFlagRGB)) ||
|
|
(state.Color.HS != nil && !cf.HasAll(device.CFlagHS))
|
|
|
|
if invalid {
|
|
var converted color.Color
|
|
var ok bool
|
|
|
|
switch {
|
|
case cf.HasAny(device.CFlagXY):
|
|
converted, ok = state.Color.ToXY()
|
|
case cf.HasAny(device.CFlagRGB):
|
|
converted, ok = state.Color.ToRGB()
|
|
case cf.HasAny(device.CFlagHS):
|
|
converted, ok = state.Color.ToHS()
|
|
}
|
|
|
|
if !ok {
|
|
state.Color = nil
|
|
} else {
|
|
state.Color = &converted
|
|
}
|
|
}
|
|
}
|
|
|
|
batch[id] = state
|
|
}
|
|
|
|
bus.RunCommand(batch)
|
|
batch = make(commands.SetStateBatch, 64)
|
|
}
|
|
}
|
|
}
|
|
|
|
type effectEnforcerRun struct {
|
|
id uuid.UUID
|
|
match string
|
|
due time.Time
|
|
ids []string
|
|
effect lucifer3.Effect
|
|
dead bool
|
|
round int
|
|
variables map[string]float64
|
|
}
|
|
|
|
// 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
|
|
}
|