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.
 
 
 
 
 
 

340 lines
7.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"
"git.aiterp.net/lucifer3/server/services/variables"
"github.com/google/uuid"
"strings"
"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),
}
return s
}
type effectEnforcer struct {
mu sync.Mutex
resolver device.Resolver
sceneMap device.SceneMap
supportFlags map[string]device.SupportFlags
colorFlags map[string]device.ColorFlags
variables variables.Variables
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.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 variables.PropertyPatch:
s.mu.Lock()
s.variables = s.variables.WithPropertyPatch(event)
s.mu.Unlock()
case events.AliasAdded:
s.triggerVariableEffects()
case events.AliasRemoved:
s.triggerVariableEffects()
case events.MotionSensed:
s.triggerVariableEffects()
case events.TemperatureChanged:
s.triggerVariableEffects()
}
}
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() {
now := time.Now()
s.mu.Lock()
for _, run := range s.list {
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 := strings.Split(vEff.VariableName(), ".")
if len(variableName) == 0 {
variableName = append(variableName, "avg")
}
var value *variables.AvgMinMax
switch variableName[0] {
case "temperature":
if value2, ok := s.variables.Temperature[run.match]; ok {
value = &value2
}
case "motion":
if value2, ok := s.variables.Motion[run.match]; ok {
value = &value2
}
}
if value != nil {
switch variableName[1] {
case "min":
state = vEff.VariableState(j, len(run.ids), value.Min)
case "max":
state = vEff.VariableState(j, len(run.ids), value.Max)
case "avg":
state = vEff.VariableState(j, len(run.ids), value.Avg)
}
}
}
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
}
// 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
}