add variable system + fix bugs here and there.

This commit is contained in:
2023-01-13 01:19:34 +01:00
parent 88036cfcae
commit 842f96d8e3
22 changed files with 368 additions and 43 deletions
@@ -1,4 +1,4 @@
package services
package effectenforcer
import (
lucifer3 "git.aiterp.net/lucifer3/server"
@@ -7,13 +7,15 @@ 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"
)
func NewEffectEnforcer(resolver device.Resolver, sceneMap device.SceneMap) lucifer3.ActiveService {
func NewService(resolver device.Resolver, sceneMap device.SceneMap) lucifer3.ActiveService {
s := &effectEnforcer{
resolver: resolver,
sceneMap: sceneMap,
@@ -34,6 +36,7 @@ type effectEnforcer struct {
supportFlags map[string]device.SupportFlags
colorFlags map[string]device.ColorFlags
variables variables.Variables
started uint32
@@ -71,6 +74,20 @@ func (s *effectEnforcer) HandleEvent(_ *lucifer3.EventBus, event lucifer3.Event)
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()
}
}
@@ -112,6 +129,7 @@ func (s *effectEnforcer) HandleCommand(bus *lucifer3.EventBus, command lucifer3.
s.mu.Lock()
// Create a new run
newRun := &effectEnforcerRun{
match: command.Match,
id: id,
due: time.Now(),
ids: allowedIDs,
@@ -141,6 +159,18 @@ func (s *effectEnforcer) HandleCommand(bus *lucifer3.EventBus, command lucifer3.
}
}
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)
@@ -169,7 +199,38 @@ 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)
}
}
}
batch[id] = state
}
if freq := run.effect.Frequency(); freq > 0 {
@@ -213,7 +274,7 @@ func (s *effectEnforcer) runLoop(bus *lucifer3.EventBus) {
}
if state.Color != nil && !sf.HasAny(device.SFlagColor) {
state.Color = nil
} else {
} 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)) ||
@@ -252,6 +313,7 @@ func (s *effectEnforcer) runLoop(bus *lucifer3.EventBus) {
type effectEnforcerRun struct {
id uuid.UUID
match string
due time.Time
ids []string
effect lucifer3.Effect
@@ -1,4 +1,4 @@
package services
package effectenforcer
import (
lucifer3 "git.aiterp.net/lucifer3/server"
@@ -9,6 +9,7 @@ import (
"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"
@@ -16,13 +17,13 @@ import (
func TestEffectEnforcer(t *testing.T) {
bus := lucifer3.EventBus{}
resolver := NewResolver()
sceneMap := NewSceneMap(resolver)
resolver := services.NewResolver()
sceneMap := services.NewSceneMap(resolver)
logger := &testutils.TestEventLogger{}
bus.JoinPrivileged(resolver)
bus.JoinPrivileged(sceneMap)
bus.Join(NewEffectEnforcer(resolver, sceneMap))
bus.Join(NewService(resolver, sceneMap))
bus.Join(logger)
for i := 1; i <= 9; i += 1 {
+6
View File
@@ -54,6 +54,10 @@ func New(addr string) (lucifer3.Service, error) {
bus.RunCommand(*input.ForgetDevice)
case input.SearchDevices != nil:
bus.RunCommand(*input.SearchDevices)
case input.AddAlias != nil:
bus.RunCommand(*input.AddAlias)
case input.RemoveAlias != nil:
bus.RunCommand(*input.RemoveAlias)
default:
return c.String(400, "No supported command found in input")
}
@@ -104,6 +108,8 @@ func (s *service) HandleEvent(bus *lucifer3.EventBus, event lucifer3.Event) {
type commandInput struct {
Assign *assignInput `json:"assign,omitempty"`
AddAlias *commands.AddAlias `json:"addAlias,omitempty"`
RemoveAlias *commands.RemoveAlias `json:"removeAlias,omitempty"`
PairDevice *commands.PairDevice `json:"pairDevice,omitempty"`
SearchDevices *commands.SearchDevices `json:"searchDevices,omitempty"`
ForgetDevice *commands.ForgetDevice `json:"forgetDevice,omitempty"`
+10 -3
View File
@@ -243,7 +243,7 @@ func (b *Bridge) ApplyPatches(date time.Time, resources []ResourceData) (eventLi
SecondsSince: 0,
})
lastMotionCopy[resource.ID] = time.Now()
lastMotionCopy[b.fullId(resource)] = time.Now()
}
}
if resource.Button != nil {
@@ -331,6 +331,13 @@ func (b *Bridge) Run(ctx context.Context, bus *lucifer3.EventBus) interface{} {
return errors.New("failed to connect to bridge")
}
bus.RunEvent(events.Log{
ID: "hue:" + b.host,
Level: "info",
Code: "hue_bridge_starting",
Message: "Bridge is connecting...",
})
go b.makeCongruentLoop(ctx)
bus.RunEvents(hwEvents)
@@ -380,8 +387,8 @@ func (b *Bridge) Run(ctx context.Context, bus *lucifer3.EventBus) interface{} {
for id, value := range lastMotion {
since := time.Since(value)
sinceMod := since % time.Minute
if since > time.Second*30 && (sinceMod <= time.Second*5) {
sinceMod := since % (time.Second * 30)
if sinceMod >= time.Second*27 || sinceMod <= time.Second*3 {
bus.RunEvent(events.MotionSensed{ID: id, SecondsSince: since.Seconds()})
}
}
+1 -1
View File
@@ -381,7 +381,7 @@ func (b *bridge) runTouchListener(ctx context.Context, bus *lucifer3.EventBus) {
goto teardownAndRetry
}
// Discard all data coming over http.
// Discard all data.go coming over http.
reqCloser = res.Body
go io.Copy(ioutil.Discard, res.Body)
+13
View File
@@ -42,6 +42,19 @@ func (r *Resolver) resolve(pattern string) []*device.Pointer {
return res
}
func (r *Resolver) GetByID(id string) *device.Pointer {
r.mu.Lock()
defer r.mu.Unlock()
for _, ptr := range r.pointers {
if ptr.ID == id {
return ptr
}
}
return nil
}
func (r *Resolver) Resolve(pattern string) []device.Pointer {
r.mu.Lock()
defer r.mu.Unlock()
+7
View File
@@ -6,12 +6,14 @@ 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 {
@@ -86,6 +88,10 @@ 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
@@ -95,6 +101,7 @@ func (d *Data) Copy() Data {
return Data{
Devices: gentools.CopyMap(d.Devices),
Assignments: gentools.CopyMap(d.Assignments),
Variables: d.Variables,
}
}
+6 -2
View File
@@ -6,12 +6,14 @@ 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"`
Assignment *AssignmentPatch `json:"assignment,omitempty"`
Device *DevicePatch `json:"device,omitempty"`
VariableProperty *variables.PropertyPatch `json:"variableProperty,omitempty"`
}
func (e Patch) VerboseKey() string {
@@ -41,6 +43,8 @@ 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"
}
+6 -1
View File
@@ -6,6 +6,7 @@ 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"
)
@@ -31,7 +32,9 @@ func (s *service) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Command
patches = []Patch{{Device: &DevicePatch{ID: command.ID, DesiredState: &command.State}}}
case commands.SetStateBatch:
for id, state := range command {
patches = []Patch{{Device: &DevicePatch{ID: id, DesiredState: gentools.ShallowCopy(&state)}}}
patches = append(patches, Patch{
Device: &DevicePatch{ID: id, DesiredState: gentools.ShallowCopy(&state)},
})
}
}
@@ -113,6 +116,8 @@ 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})
}
if len(patches) > 0 {
+27
View File
@@ -0,0 +1,27 @@
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
View File
@@ -0,0 +1,17 @@
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
View File
@@ -0,0 +1,103 @@
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
}