From 842f96d8e360d1c6a003f965be55ceb53c32b724 Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Fri, 13 Jan 2023 01:19:34 +0100 Subject: [PATCH] add variable system + fix bugs here and there. --- cmd/bustest/main.go | 3 +- cmd/lucifer4-server/main.go | 5 +- commands/edit.go | 8 +- device/pointer.go | 2 + device/state.go | 14 ++- effects/serializable.go | 5 + effects/vrange.go | 46 ++++++++ interface.go | 7 ++ internal/color/color.go | 39 ++++--- internal/gentools/maps.go | 2 +- .../service.go} | 68 +++++++++++- .../service_test.go} | 9 +- services/httpapiv1/service.go | 6 + services/hue/bridge.go | 13 ++- services/nanoleaf/client.go | 2 +- services/resolver.go | 13 +++ services/uistate/data.go | 7 ++ services/uistate/patch.go | 8 +- services/uistate/service.go | 7 +- services/variables/data.go | 27 +++++ services/variables/patch.go | 17 +++ services/variables/service.go | 103 ++++++++++++++++++ 22 files changed, 368 insertions(+), 43 deletions(-) create mode 100644 effects/vrange.go rename services/{effectenforcer.go => effectenforcer/service.go} (78%) rename services/{effectenforcer_test.go => effectenforcer/service_test.go} (90%) create mode 100644 services/variables/data.go create mode 100644 services/variables/patch.go create mode 100644 services/variables/service.go diff --git a/cmd/bustest/main.go b/cmd/bustest/main.go index 86f3205..66ed9aa 100644 --- a/cmd/bustest/main.go +++ b/cmd/bustest/main.go @@ -3,6 +3,7 @@ package main import ( lucifer3 "git.aiterp.net/lucifer3/server" "git.aiterp.net/lucifer3/server/services" + "git.aiterp.net/lucifer3/server/services/effectenforcer" "git.aiterp.net/lucifer3/server/services/hue" "git.aiterp.net/lucifer3/server/services/mill" "git.aiterp.net/lucifer3/server/services/nanoleaf" @@ -18,7 +19,7 @@ func main() { bus.JoinPrivileged(resolver) bus.JoinPrivileged(sceneMap) - bus.Join(services.NewEffectEnforcer(resolver, sceneMap)) + bus.Join(effectenforcer.NewService(resolver, sceneMap)) bus.Join(nanoleaf.NewService()) bus.Join(hue.NewService()) bus.Join(tradfri.NewService()) diff --git a/cmd/lucifer4-server/main.go b/cmd/lucifer4-server/main.go index 31a682d..9f5fa33 100644 --- a/cmd/lucifer4-server/main.go +++ b/cmd/lucifer4-server/main.go @@ -4,11 +4,13 @@ import ( lucifer3 "git.aiterp.net/lucifer3/server" "git.aiterp.net/lucifer3/server/events" "git.aiterp.net/lucifer3/server/services" + "git.aiterp.net/lucifer3/server/services/effectenforcer" "git.aiterp.net/lucifer3/server/services/httpapiv1" "git.aiterp.net/lucifer3/server/services/hue" "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" @@ -40,10 +42,11 @@ func main() { bus.JoinPrivileged(resolver) bus.JoinPrivileged(sceneMap) - bus.Join(services.NewEffectEnforcer(resolver, sceneMap)) + bus.Join(effectenforcer.NewService(resolver, sceneMap)) 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/commands/edit.go b/commands/edit.go index 56baf45..643cca4 100644 --- a/commands/edit.go +++ b/commands/edit.go @@ -5,8 +5,8 @@ import ( ) type AddAlias struct { - Match string - Alias string + Match string `json:"match"` + Alias string `json:"alias"` } func (c AddAlias) CommandDescription() string { @@ -14,8 +14,8 @@ func (c AddAlias) CommandDescription() string { } type RemoveAlias struct { - Match string - Alias string + Match string `json:"match"` + Alias string `json:"alias"` } func (c RemoveAlias) CommandDescription() string { diff --git a/device/pointer.go b/device/pointer.go index 0c0f317..4209ac7 100644 --- a/device/pointer.go +++ b/device/pointer.go @@ -28,6 +28,7 @@ func (p *Pointer) AddAlias(alias string) (added *string, removed *string) { if strings.HasPrefix(alias, "lucifer:name:") { for i, alias2 := range p.Aliases { if strings.HasPrefix(alias2, "lucifer:name:") { + p.Aliases = append(p.Aliases[:0:0], p.Aliases...) p.Aliases = append(p.Aliases[:i], p.Aliases[i+1:]...) removed = gentools.ShallowCopy(&alias2) break @@ -48,6 +49,7 @@ func (p *Pointer) RemoveAlias(alias string) bool { for i, alias2 := range p.Aliases { if alias2 == alias { + p.Aliases = append(p.Aliases[:0:0], p.Aliases...) p.Aliases = append(p.Aliases[:i], p.Aliases[i+1:]...) return true } diff --git a/device/state.go b/device/state.go index 8d46ba1..3964cf5 100644 --- a/device/state.go +++ b/device/state.go @@ -70,10 +70,16 @@ func (s State) Interpolate(s2 State, f float64) State { } if s.Power != nil && s2.Power != nil { - if f < 0.5 { - newState.Power = gentools.ShallowCopy(s.Power) - } else { - newState.Power = gentools.ShallowCopy(s2.Power) + if *s.Power == *s2.Power { + newState.Power = s.Power + } + + // With power, interpolate strongly in the direction of power + if *s.Power { + newState.Power = gentools.Ptr(f < 0.95) + } + if *s2.Power { + newState.Power = gentools.Ptr(f > 0.05) } } else if s.Power != nil { newState.Power = gentools.ShallowCopy(s.Power) diff --git a/effects/serializable.go b/effects/serializable.go index 5524ad7..b418d25 100644 --- a/effects/serializable.go +++ b/effects/serializable.go @@ -11,6 +11,7 @@ type serializedEffect struct { Gradient *Gradient `json:"gradient,omitempty"` Pattern *Pattern `json:"pattern,omitempty"` Random *Random `json:"random,omitempty"` + VRange *VRange `json:"vrange,omitempty"` } type Serializable struct { @@ -33,6 +34,8 @@ func (s *Serializable) UnmarshalJSON(raw []byte) error { s.Effect = *value.Pattern case value.Random != nil: s.Effect = *value.Random + case value.VRange != nil: + s.Effect = *value.VRange default: return errors.New("unsupported effect") } @@ -50,6 +53,8 @@ func (s *Serializable) MarshalJSON() ([]byte, error) { return json.Marshal(serializedEffect{Pattern: &effect}) case Random: return json.Marshal(serializedEffect{Random: &effect}) + case VRange: + return json.Marshal(serializedEffect{VRange: &effect}) default: panic(s.Effect.EffectDescription() + "is not understood by serializer") } diff --git a/effects/vrange.go b/effects/vrange.go new file mode 100644 index 0000000..f6c90f8 --- /dev/null +++ b/effects/vrange.go @@ -0,0 +1,46 @@ +package effects + +import ( + "fmt" + "git.aiterp.net/lucifer3/server/device" + "time" +) + +type VRange struct { + States []device.State `json:"states,omitempty"` + Variable string `json:"variable"` + Min float64 `json:"min"` + Max float64 `json:"max"` +} + +func (e VRange) VariableName() string { + return e.Variable +} + +func (e VRange) VariableState(_, _ int, value float64) device.State { + if value <= e.Min { + return e.States[0] + } else if value >= e.Max { + return e.States[len(e.States)-1] + } + + return gradientStateFactor(e.States, true, (value-e.Min)/(e.Max-e.Min)) +} + +func (e VRange) State(_, _, _ int) device.State { + if len(e.States) == 0 { + return device.State{} + } + return e.States[0] +} + +func (e VRange) Frequency() time.Duration { + return 0 +} + +func (e VRange) EffectDescription() string { + return fmt.Sprintf( + "VRange(states:%s, name:%s, range:%.1f..%.1f)", + statesDescription(e.States), e.Variable, e.Min, e.Max, + ) +} diff --git a/interface.go b/interface.go index 152aac8..44c86be 100644 --- a/interface.go +++ b/interface.go @@ -14,3 +14,10 @@ type Effect interface { Frequency() time.Duration EffectDescription() string } + +type VariableEffect interface { + Effect + + VariableName() string + VariableState(index, len int, value float64) device.State +} diff --git a/internal/color/color.go b/internal/color/color.go index 52443d8..b0bcfd6 100644 --- a/internal/color/color.go +++ b/internal/color/color.go @@ -3,7 +3,9 @@ package color import ( "encoding/json" "fmt" + "git.aiterp.net/lucifer3/server/internal/gentools" "github.com/lucasb-eyer/go-colorful" + "log" "math" "strconv" "strings" @@ -56,10 +58,6 @@ func (col *Color) SetXY(xy XY) { } func (col *Color) AlmostEquals(other Color) bool { - if (col.K != nil) != (other.K != nil) { - return false - } - if col.HS != nil && other.HS != nil { if math.Abs(col.HS.Hue-other.HS.Hue) > 0.01 { return false @@ -68,7 +66,7 @@ func (col *Color) AlmostEquals(other Color) bool { return false } - if col.K != nil && *col.K != *other.K { + if col.K != nil && other.K != nil && *col.K != *other.K { return false } @@ -76,6 +74,10 @@ func (col *Color) AlmostEquals(other Color) bool { } if col.K != nil { + if other.K != nil { + return false + } + return *col.K == *other.K } @@ -94,15 +96,8 @@ func (col *Color) AlmostEquals(other Color) bool { } xy1, _ := col.ToXY() - xy2, _ := col.ToXY() - if math.Abs(xy1.XY.X-xy2.XY.X) > 0.001 { - return false - } - if math.Abs(xy1.XY.Y-xy2.XY.Y) > 0.001 { - return false - } - - return true + xy2, _ := other.ToXY() + return math.Abs(xy1.XY.X-xy2.XY.X)+math.Abs(xy1.XY.Y-xy2.XY.Y) < 0.001 } // ToRGB tries to copy the color to an RGB color. If it's already RGB, it will be plainly copied, but HS @@ -217,15 +212,20 @@ func (col *Color) ToXY() (col2 Color, ok bool) { func (col *Color) Interpolate(other Color, fac float64) Color { if col.AlmostEquals(other) { + log.Println("ALMOST EQUAL", col.String(), other.String()) return *col } - // Special case for kelvin values. + // Special case for both kelvin values. if col.IsKelvin() && other.IsKelvin() { k1 := *col.K - k2 := *col.K - k3 := k1 + int(float64(k2-k1)*fac) - return Color{K: &k3} + k2 := *other.K + + if k2 > k1 { + return Color{K: gentools.Ptr(k1 + int(float64(k2-k1)*fac))} + } else { + return Color{K: gentools.Ptr(k1 - int(float64(k1-k2)*fac))} + } } if fac < 0.000001 { @@ -292,6 +292,9 @@ func (col *Color) String() string { func (col *Color) colorful() colorful.Color { switch { + case col.K != nil: + col2, _ := col.ToXY() + return col2.colorful() case col.HS != nil: return colorful.Hsv(col.HS.Hue, col.HS.Sat, 1) case col.RGB != nil: diff --git a/internal/gentools/maps.go b/internal/gentools/maps.go index ded77b4..1f2a792 100644 --- a/internal/gentools/maps.go +++ b/internal/gentools/maps.go @@ -1,7 +1,7 @@ package gentools func CopyMap[K comparable, V any](m map[K]V) map[K]V { - m2 := make(map[K]V, len(m)) + m2 := make(map[K]V, len(m)+1) for k, v := range m { m2[k] = v } diff --git a/services/effectenforcer.go b/services/effectenforcer/service.go similarity index 78% rename from services/effectenforcer.go rename to services/effectenforcer/service.go index 22a5bda..e3eee05 100644 --- a/services/effectenforcer.go +++ b/services/effectenforcer/service.go @@ -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 diff --git a/services/effectenforcer_test.go b/services/effectenforcer/service_test.go similarity index 90% rename from services/effectenforcer_test.go rename to services/effectenforcer/service_test.go index 089f0c9..6d435a0 100644 --- a/services/effectenforcer_test.go +++ b/services/effectenforcer/service_test.go @@ -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 { diff --git a/services/httpapiv1/service.go b/services/httpapiv1/service.go index 9f6913e..ad6451e 100644 --- a/services/httpapiv1/service.go +++ b/services/httpapiv1/service.go @@ -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"` diff --git a/services/hue/bridge.go b/services/hue/bridge.go index 5f7ecb9..c9eddb7 100644 --- a/services/hue/bridge.go +++ b/services/hue/bridge.go @@ -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()}) } } diff --git a/services/nanoleaf/client.go b/services/nanoleaf/client.go index 8c68bc4..8f66fae 100644 --- a/services/nanoleaf/client.go +++ b/services/nanoleaf/client.go @@ -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) diff --git a/services/resolver.go b/services/resolver.go index b63bf00..c44b657 100644 --- a/services/resolver.go +++ b/services/resolver.go @@ -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() diff --git a/services/uistate/data.go b/services/uistate/data.go index af920c1..afe0858 100644 --- a/services/uistate/data.go +++ b/services/uistate/data.go @@ -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, } } diff --git a/services/uistate/patch.go b/services/uistate/patch.go index 3260c8e..6cfc9b9 100644 --- a/services/uistate/patch.go +++ b/services/uistate/patch.go @@ -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" } diff --git a/services/uistate/service.go b/services/uistate/service.go index ca5ec89..36f183b 100644 --- a/services/uistate/service.go +++ b/services/uistate/service.go @@ -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 { diff --git a/services/variables/data.go b/services/variables/data.go new file mode 100644 index 0000000..3b56c73 --- /dev/null +++ b/services/variables/data.go @@ -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"` +} diff --git a/services/variables/patch.go b/services/variables/patch.go new file mode 100644 index 0000000..15b4d81 --- /dev/null +++ b/services/variables/patch.go @@ -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) +} diff --git a/services/variables/service.go b/services/variables/service.go new file mode 100644 index 0000000..4a9aad7 --- /dev/null +++ b/services/variables/service.go @@ -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 +}