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.
 
 
 
 
 
 

377 lines
8.6 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, sceneMap device.SceneMap) lucifer3.ActiveService {
s := &effectEnforcer{
resolver: resolver,
sceneMap: sceneMap,
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
sceneMap device.SceneMap
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 be
// re-ran.
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)
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{
match: command.Match,
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) 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
}