diff --git a/cmd/lucifer4-server/main.go b/cmd/lucifer4-server/main.go index 9f5fa33..555f7a6 100644 --- a/cmd/lucifer4-server/main.go +++ b/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) diff --git a/events/assignment.go b/events/assignment.go index 37906d4..c00c77d 100644 --- a/events/assignment.go +++ b/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), ","), + ) +} diff --git a/internal/gentools/maps.go b/internal/gentools/maps.go index 1f2a792..eb9bb29 100644 --- a/internal/gentools/maps.go +++ b/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 +} \ No newline at end of file diff --git a/services/effectenforcer/service.go b/services/effectenforcer/service.go index e3eee05..e9f0385 100644 --- a/services/effectenforcer/service.go +++ b/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. diff --git a/services/hue/bridge.go b/services/hue/bridge.go index bed30af..1ed90d0 100644 --- a/services/hue/bridge.go +++ b/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()}) } } diff --git a/services/uistate/data.go b/services/uistate/data.go index afe0858..cc9155d 100644 --- a/services/uistate/data.go +++ b/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"` } diff --git a/services/uistate/patch.go b/services/uistate/patch.go index 6cfc9b9..1b43222 100644 --- a/services/uistate/patch.go +++ b/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"` } diff --git a/services/uistate/service.go b/services/uistate/service.go index 36f183b..a886851 100644 --- a/services/uistate/service.go +++ b/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 { diff --git a/services/variables/data.go b/services/variables/data.go deleted file mode 100644 index 3b56c73..0000000 --- a/services/variables/data.go +++ /dev/null @@ -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"` -} diff --git a/services/variables/patch.go b/services/variables/patch.go deleted file mode 100644 index 15b4d81..0000000 --- a/services/variables/patch.go +++ /dev/null @@ -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) -} diff --git a/services/variables/service.go b/services/variables/service.go deleted file mode 100644 index 4a9aad7..0000000 --- a/services/variables/service.go +++ /dev/null @@ -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 -}