Browse Source

refactor variable system to be the responsibility of effect enforcer.

beelzebub
Gisle Aune 1 year ago
parent
commit
d2b2244e2f
  1. 2
      cmd/lucifer4-server/main.go
  2. 13
      events/assignment.go
  3. 9
      internal/gentools/maps.go
  4. 133
      services/effectenforcer/service.go
  5. 2
      services/hue/bridge.go
  6. 11
      services/uistate/data.go
  7. 19
      services/uistate/patch.go
  8. 9
      services/uistate/service.go
  9. 27
      services/variables/data.go
  10. 17
      services/variables/patch.go
  11. 103
      services/variables/service.go

2
cmd/lucifer4-server/main.go

@ -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)

13
events/assignment.go

@ -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), ","),
)
}

9
internal/gentools/maps.go

@ -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
}

133
services/effectenforcer/service.go

@ -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.

2
services/hue/bridge.go

@ -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()})
}
}

11
services/uistate/data.go

@ -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"`
}

19
services/uistate/patch.go

@ -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"`
}

9
services/uistate/service.go

@ -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 {

27
services/variables/data.go

@ -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"`
}

17
services/variables/patch.go

@ -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)
}

103
services/variables/service.go

@ -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
}
Loading…
Cancel
Save