add variable system + fix bugs here and there.
This commit is contained in:
@@ -0,0 +1,340 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
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/effects"
|
||||
"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/internal/testutils"
|
||||
"git.aiterp.net/lucifer3/server/services"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestEffectEnforcer(t *testing.T) {
|
||||
bus := lucifer3.EventBus{}
|
||||
resolver := services.NewResolver()
|
||||
sceneMap := services.NewSceneMap(resolver)
|
||||
logger := &testutils.TestEventLogger{}
|
||||
|
||||
bus.JoinPrivileged(resolver)
|
||||
bus.JoinPrivileged(sceneMap)
|
||||
bus.Join(NewService(resolver, sceneMap))
|
||||
bus.Join(logger)
|
||||
|
||||
for i := 1; i <= 9; i += 1 {
|
||||
bus.RunEvents([]lucifer3.Event{
|
||||
events.HardwareState{
|
||||
ID: "test:" + strconv.Itoa(i),
|
||||
InternalName: "Test Device " + strconv.Itoa(i),
|
||||
SupportFlags: device.SFlagColor | device.SFlagIntensity | device.SFlagPower,
|
||||
ColorFlags: device.CFlagXY | device.CFlagHS | device.CFlagRGB | device.CFlagKelvin,
|
||||
ColorGamut: nil,
|
||||
ColorKelvinRange: nil,
|
||||
Buttons: nil,
|
||||
State: device.State{
|
||||
Power: gentools.Ptr(false),
|
||||
Intensity: gentools.Ptr(1.0),
|
||||
Color: &color.Color{K: gentools.Ptr(2900)},
|
||||
},
|
||||
BatteryPercentage: nil,
|
||||
Unreachable: false,
|
||||
},
|
||||
events.DeviceReady{ID: "test:" + strconv.Itoa(i)},
|
||||
})
|
||||
}
|
||||
|
||||
bus.RunCommand(commands.Assign{
|
||||
Match: "test:*",
|
||||
Effect: effects.Pattern{
|
||||
States: []device.State{
|
||||
{Color: &color.Color{XY: &color.XY{X: 0.22, Y: 0.18}}},
|
||||
{Color: &color.Color{XY: &color.XY{X: 0.27, Y: 0.23}}},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
logger.Sync(&bus)
|
||||
logger.AssertEvent(t, "DeviceReady(id:test:1)")
|
||||
logger.AssertEvent(t, "DeviceReady(id:test:9)")
|
||||
logger.AssertCommand(t, "Assign(test:*, Pattern(states:[(xy:0.2200,0.1800), (xy:0.2700,0.2300)], anim:0ms))")
|
||||
logger.AssertCommand(t, "SetStateBatch(9 devices)")
|
||||
}
|
||||
Reference in New Issue
Block a user