refactor variable system to be the responsibility of effect enforcer.
This commit is contained in:
@@ -10,7 +10,6 @@ import (
|
||||
"git.aiterp.net/lucifer3/server/services/mysqldb"
|
||||
"git.aiterp.net/lucifer3/server/services/nanoleaf"
|
||||
"git.aiterp.net/lucifer3/server/services/uistate"
|
||||
"git.aiterp.net/lucifer3/server/services/variables"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -46,7 +45,6 @@ func main() {
|
||||
bus.Join(nanoleaf.NewService())
|
||||
bus.Join(hue.NewService())
|
||||
bus.Join(uistate.NewService())
|
||||
bus.Join(variables.NewService(resolver))
|
||||
bus.Join(database)
|
||||
bus.Join(httpAPI)
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ package events
|
||||
import (
|
||||
"fmt"
|
||||
lucifer3 "git.aiterp.net/lucifer3/server"
|
||||
"git.aiterp.net/lucifer3/server/internal/gentools"
|
||||
"github.com/google/uuid"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type AssignmentCreated struct {
|
||||
@@ -32,3 +34,14 @@ type DeviceAssigned struct {
|
||||
func (e DeviceAssigned) EventDescription() string {
|
||||
return fmt.Sprintf("DeviceAssigned(device=%s, %s)", e.DeviceID, e.AssignmentID)
|
||||
}
|
||||
|
||||
type AssignmentVariables struct {
|
||||
ID uuid.UUID
|
||||
Map map[string]float64
|
||||
}
|
||||
|
||||
func (e AssignmentVariables) EventDescription() string {
|
||||
return fmt.Sprintf("AssignmentVariables(id=%s, keys=[%s])",
|
||||
e.ID, strings.Join(gentools.MapKeys(e.Map), ","),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,3 +14,12 @@ func OneItemMap[K comparable, V any](key K, value V) map[K]V {
|
||||
m[key] = value
|
||||
return m
|
||||
}
|
||||
|
||||
func MapKeys[K comparable, V any](m map[K]V) []K {
|
||||
keys := make([]K, 0, len(m))
|
||||
for key := range m {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
@@ -7,9 +7,7 @@ import (
|
||||
"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"
|
||||
@@ -24,6 +22,8 @@ func NewService(resolver device.Resolver, sceneMap device.SceneMap) lucifer3.Act
|
||||
|
||||
supportFlags: make(map[string]device.SupportFlags),
|
||||
colorFlags: make(map[string]device.ColorFlags),
|
||||
temperatures: make(map[string]float64),
|
||||
motions: make(map[string]float64),
|
||||
}
|
||||
|
||||
return s
|
||||
@@ -36,7 +36,8 @@ type effectEnforcer struct {
|
||||
|
||||
supportFlags map[string]device.SupportFlags
|
||||
colorFlags map[string]device.ColorFlags
|
||||
variables variables.Variables
|
||||
temperatures map[string]float64
|
||||
motions map[string]float64
|
||||
|
||||
started uint32
|
||||
|
||||
@@ -48,7 +49,7 @@ func (s *effectEnforcer) Active() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *effectEnforcer) HandleEvent(_ *lucifer3.EventBus, event lucifer3.Event) {
|
||||
func (s *effectEnforcer) HandleEvent(bus *lucifer3.EventBus, event lucifer3.Event) {
|
||||
switch event := event.(type) {
|
||||
case events.HardwareState:
|
||||
s.mu.Lock()
|
||||
@@ -75,19 +76,21 @@ func (s *effectEnforcer) HandleEvent(_ *lucifer3.EventBus, event lucifer3.Event)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
case variables.PropertyPatch:
|
||||
case events.DeviceAssigned:
|
||||
s.triggerVariableEffects(bus, event.DeviceID)
|
||||
|
||||
case events.MotionSensed:
|
||||
s.mu.Lock()
|
||||
s.variables = s.variables.WithPropertyPatch(event)
|
||||
s.motions[event.ID] = event.SecondsSince
|
||||
s.mu.Unlock()
|
||||
|
||||
case events.AliasAdded:
|
||||
s.triggerVariableEffects()
|
||||
case events.AliasRemoved:
|
||||
s.triggerVariableEffects()
|
||||
case events.MotionSensed:
|
||||
s.triggerVariableEffects()
|
||||
s.triggerVariableEffects(bus, event.ID)
|
||||
case events.TemperatureChanged:
|
||||
s.triggerVariableEffects()
|
||||
s.mu.Lock()
|
||||
s.temperatures[event.ID] = event.Temperature
|
||||
s.mu.Unlock()
|
||||
|
||||
s.triggerVariableEffects(bus, event.ID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,11 +162,68 @@ func (s *effectEnforcer) HandleCommand(bus *lucifer3.EventBus, command lucifer3.
|
||||
}
|
||||
}
|
||||
|
||||
func (s *effectEnforcer) triggerVariableEffects() {
|
||||
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
|
||||
}
|
||||
@@ -200,37 +260,13 @@ func (s *effectEnforcer) runLoop(bus *lucifer3.EventBus) {
|
||||
|
||||
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)
|
||||
}
|
||||
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 {
|
||||
@@ -312,13 +348,14 @@ func (s *effectEnforcer) runLoop(bus *lucifer3.EventBus) {
|
||||
}
|
||||
|
||||
type effectEnforcerRun struct {
|
||||
id uuid.UUID
|
||||
match string
|
||||
due time.Time
|
||||
ids []string
|
||||
effect lucifer3.Effect
|
||||
dead bool
|
||||
round int
|
||||
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.
|
||||
|
||||
@@ -388,7 +388,7 @@ func (b *Bridge) Run(ctx context.Context, bus *lucifer3.EventBus) interface{} {
|
||||
for id, value := range lastMotion {
|
||||
since := time.Since(value)
|
||||
sinceMod := since % (time.Second * 30)
|
||||
if sinceMod >= time.Second*27 || sinceMod <= time.Second*3 {
|
||||
if (since > time.Second*20) && (sinceMod >= time.Second*27 || sinceMod <= time.Second*3) {
|
||||
bus.RunEvent(events.MotionSensed{ID: id, SecondsSince: since.Seconds()})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,12 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
type Data struct {
|
||||
Devices map[string]Device `json:"devices"`
|
||||
Assignments map[uuid.UUID]Assignment `json:"assignments"`
|
||||
Variables variables.Variables `json:"variables"`
|
||||
}
|
||||
|
||||
func (d *Data) WithPatch(patches ...Patch) Data {
|
||||
@@ -81,6 +79,9 @@ func (d *Data) WithPatch(patches ...Patch) Data {
|
||||
}
|
||||
}
|
||||
}
|
||||
if patch.Assignment.Variables != nil {
|
||||
pa.Variables = gentools.CopyMap(patch.Assignment.Variables)
|
||||
}
|
||||
|
||||
if patch.Assignment.Delete {
|
||||
delete(newData.Assignments, pa.ID)
|
||||
@@ -88,10 +89,6 @@ func (d *Data) WithPatch(patches ...Patch) Data {
|
||||
newData.Assignments[pa.ID] = pa
|
||||
}
|
||||
}
|
||||
|
||||
if patch.VariableProperty != nil {
|
||||
newData.Variables = newData.Variables.WithPropertyPatch(*patch.VariableProperty)
|
||||
}
|
||||
}
|
||||
|
||||
return newData
|
||||
@@ -101,7 +98,6 @@ func (d *Data) Copy() Data {
|
||||
return Data{
|
||||
Devices: gentools.CopyMap(d.Devices),
|
||||
Assignments: gentools.CopyMap(d.Assignments),
|
||||
Variables: d.Variables,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,4 +138,5 @@ type Assignment struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
DeviceIDs []string `json:"deviceIds"`
|
||||
Effect *effects.Serializable `json:"effect"`
|
||||
Variables map[string]float64 `json:"variables"`
|
||||
}
|
||||
|
||||
@@ -6,14 +6,12 @@ import (
|
||||
"git.aiterp.net/lucifer3/server/effects"
|
||||
"git.aiterp.net/lucifer3/server/events"
|
||||
"git.aiterp.net/lucifer3/server/internal/color"
|
||||
"git.aiterp.net/lucifer3/server/services/variables"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Patch struct {
|
||||
Assignment *AssignmentPatch `json:"assignment,omitempty"`
|
||||
Device *DevicePatch `json:"device,omitempty"`
|
||||
VariableProperty *variables.PropertyPatch `json:"variableProperty,omitempty"`
|
||||
Assignment *AssignmentPatch `json:"assignment,omitempty"`
|
||||
Device *DevicePatch `json:"device,omitempty"`
|
||||
}
|
||||
|
||||
func (e Patch) VerboseKey() string {
|
||||
@@ -24,7 +22,7 @@ func (e Patch) EventDescription() string {
|
||||
if e.Device != nil {
|
||||
switch {
|
||||
case e.Device.Name != nil:
|
||||
return fmt.Sprintf("uistate.Patch(device=%s, name=%s)", e.Device.ID, e.Device.Name)
|
||||
return fmt.Sprintf("uistate.Patch(device=%s, name=%s)", e.Device.ID, *e.Device.Name)
|
||||
case e.Device.DesiredState != nil:
|
||||
return fmt.Sprintf("uistate.Patch(device=%s, desiredState=%s)", e.Device.ID, e.Device.DesiredState.String())
|
||||
case e.Device.HWState != nil:
|
||||
@@ -43,8 +41,6 @@ func (e Patch) EventDescription() string {
|
||||
}
|
||||
} else if e.Assignment != nil {
|
||||
return fmt.Sprintf("uistate.Patch(assignment=%s)", e.Assignment.ID)
|
||||
} else if e.VariableProperty != nil {
|
||||
return fmt.Sprintf("uistate.Patch(variableProperty=%s)", e.VariableProperty.Key)
|
||||
} else {
|
||||
return "uistate.Patch"
|
||||
}
|
||||
@@ -69,8 +65,9 @@ type DevicePatch struct {
|
||||
|
||||
type AssignmentPatch struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
AddDeviceID *string `json:"addDeviceId"`
|
||||
RemoveDeviceID *string `json:"removeDeviceId"`
|
||||
Effect *effects.Serializable `json:"effect"`
|
||||
Delete bool `json:"delete"`
|
||||
AddDeviceID *string `json:"addDeviceId,omitempty"`
|
||||
RemoveDeviceID *string `json:"removeDeviceId,omitempty"`
|
||||
Effect *effects.Serializable `json:"effect,omitempty"`
|
||||
Variables map[string]float64 `json:"variables,omitempty"`
|
||||
Delete bool `json:"delete,omitempty"`
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"git.aiterp.net/lucifer3/server/effects"
|
||||
"git.aiterp.net/lucifer3/server/events"
|
||||
"git.aiterp.net/lucifer3/server/internal/gentools"
|
||||
"git.aiterp.net/lucifer3/server/services/variables"
|
||||
"sync"
|
||||
)
|
||||
|
||||
@@ -116,8 +115,12 @@ func (s *service) HandleEvent(bus *lucifer3.EventBus, event lucifer3.Event) {
|
||||
Assignment: gentools.ShallowCopy(event.AssignmentID),
|
||||
ClearAssignment: event.AssignmentID == nil,
|
||||
}})
|
||||
case variables.PropertyPatch:
|
||||
patches = append(patches, Patch{VariableProperty: &event})
|
||||
case events.AssignmentVariables:
|
||||
// Always set the assignment
|
||||
patches = append(patches, Patch{Assignment: &AssignmentPatch{
|
||||
ID: event.ID,
|
||||
Variables: event.Map,
|
||||
}})
|
||||
}
|
||||
|
||||
if len(patches) > 0 {
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package variables
|
||||
|
||||
import "git.aiterp.net/lucifer3/server/internal/gentools"
|
||||
|
||||
type Variables struct {
|
||||
Temperature map[string]AvgMinMax `json:"temperature"`
|
||||
Motion map[string]AvgMinMax `json:"motion"`
|
||||
}
|
||||
|
||||
func (v Variables) WithPropertyPatch(patch PropertyPatch) Variables {
|
||||
if patch.Motion != nil {
|
||||
v.Motion = gentools.CopyMap(v.Motion)
|
||||
v.Motion[patch.Key] = *patch.Motion
|
||||
}
|
||||
if patch.Temperature != nil {
|
||||
v.Temperature = gentools.CopyMap(v.Temperature)
|
||||
v.Temperature[patch.Key] = *patch.Temperature
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
type AvgMinMax struct {
|
||||
Avg float64 `json:"avg"`
|
||||
Min float64 `json:"min"`
|
||||
Max float64 `json:"max"`
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package variables
|
||||
|
||||
import "fmt"
|
||||
|
||||
type PropertyPatch struct {
|
||||
Key string `json:"id"`
|
||||
Temperature *AvgMinMax `json:"temperature,omitempty"`
|
||||
Motion *AvgMinMax `json:"motion,omitempty"`
|
||||
}
|
||||
|
||||
func (e PropertyPatch) VerboseKey() string {
|
||||
return "variables.PropertyPatch"
|
||||
}
|
||||
|
||||
func (e PropertyPatch) EventDescription() string {
|
||||
return fmt.Sprintf("variables.PropertyPatch(key=%s)", e.Key)
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package variables
|
||||
|
||||
import (
|
||||
lucifer3 "git.aiterp.net/lucifer3/server"
|
||||
"git.aiterp.net/lucifer3/server/events"
|
||||
"git.aiterp.net/lucifer3/server/services"
|
||||
)
|
||||
|
||||
func NewService(resolver *services.Resolver) lucifer3.Service {
|
||||
return &service{
|
||||
resolver: resolver,
|
||||
temperatures: map[string]float64{},
|
||||
motions: map[string]float64{},
|
||||
}
|
||||
}
|
||||
|
||||
type service struct {
|
||||
resolver *services.Resolver
|
||||
temperatures map[string]float64
|
||||
motions map[string]float64
|
||||
}
|
||||
|
||||
func (s *service) Active() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *service) HandleEvent(bus *lucifer3.EventBus, event lucifer3.Event) {
|
||||
var patchEvents []lucifer3.Event
|
||||
|
||||
switch event := event.(type) {
|
||||
case events.MotionSensed:
|
||||
s.motions[event.ID] = event.SecondsSince
|
||||
patchEvents = s.updateDevice(event.ID)
|
||||
case events.TemperatureChanged:
|
||||
s.temperatures[event.ID] = event.Temperature
|
||||
patchEvents = s.updateDevice(event.ID)
|
||||
case events.AliasAdded:
|
||||
patchEvents = s.updateDevice(event.ID)
|
||||
case events.AliasRemoved:
|
||||
patchEvents = s.updateDevice(event.ID)
|
||||
}
|
||||
|
||||
bus.RunEvents(patchEvents)
|
||||
}
|
||||
|
||||
func (s *service) updateDevice(id string) []lucifer3.Event {
|
||||
ptr := s.resolver.GetByID(id)
|
||||
if ptr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
res := make([]lucifer3.Event, 0, len(ptr.Aliases)+1)
|
||||
|
||||
for _, alias := range append([]string{ptr.ID}, ptr.Aliases...) {
|
||||
patch := PropertyPatch{Key: alias}
|
||||
motionSamples := 0.0
|
||||
motionTotal := 0.0
|
||||
temperatureSamples := 0.0
|
||||
temperatureTotal := 0.0
|
||||
|
||||
for _, ptr := range s.resolver.Resolve(alias) {
|
||||
if temp, ok := s.temperatures[ptr.ID]; ok {
|
||||
if patch.Temperature == nil {
|
||||
patch.Temperature = &AvgMinMax{}
|
||||
if temp < patch.Temperature.Min || temperatureSamples == 0.0 {
|
||||
patch.Temperature.Min = temp
|
||||
}
|
||||
if temp > patch.Temperature.Max {
|
||||
patch.Temperature.Max = temp
|
||||
}
|
||||
|
||||
temperatureSamples += 1.0
|
||||
temperatureTotal += temp
|
||||
}
|
||||
}
|
||||
if motion, ok := s.motions[ptr.ID]; ok {
|
||||
if patch.Motion == nil {
|
||||
patch.Motion = &AvgMinMax{}
|
||||
if motion < patch.Motion.Min || motionSamples == 0.0 {
|
||||
patch.Motion.Min = motion
|
||||
}
|
||||
if motion > patch.Motion.Max {
|
||||
patch.Motion.Max = motion
|
||||
}
|
||||
|
||||
motionSamples += 1.0
|
||||
motionTotal += motion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if temperatureSamples > 0.5 {
|
||||
patch.Temperature.Avg = temperatureTotal / temperatureSamples
|
||||
}
|
||||
if motionSamples > 0.5 {
|
||||
patch.Motion.Avg = motionTotal / motionSamples
|
||||
}
|
||||
|
||||
res = append(res, patch)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
Reference in New Issue
Block a user