add variable system + fix bugs here and there.
This commit is contained in:
+2
-1
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
lucifer3 "git.aiterp.net/lucifer3/server"
|
lucifer3 "git.aiterp.net/lucifer3/server"
|
||||||
"git.aiterp.net/lucifer3/server/services"
|
"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/hue"
|
||||||
"git.aiterp.net/lucifer3/server/services/mill"
|
"git.aiterp.net/lucifer3/server/services/mill"
|
||||||
"git.aiterp.net/lucifer3/server/services/nanoleaf"
|
"git.aiterp.net/lucifer3/server/services/nanoleaf"
|
||||||
@@ -18,7 +19,7 @@ func main() {
|
|||||||
|
|
||||||
bus.JoinPrivileged(resolver)
|
bus.JoinPrivileged(resolver)
|
||||||
bus.JoinPrivileged(sceneMap)
|
bus.JoinPrivileged(sceneMap)
|
||||||
bus.Join(services.NewEffectEnforcer(resolver, sceneMap))
|
bus.Join(effectenforcer.NewService(resolver, sceneMap))
|
||||||
bus.Join(nanoleaf.NewService())
|
bus.Join(nanoleaf.NewService())
|
||||||
bus.Join(hue.NewService())
|
bus.Join(hue.NewService())
|
||||||
bus.Join(tradfri.NewService())
|
bus.Join(tradfri.NewService())
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import (
|
|||||||
lucifer3 "git.aiterp.net/lucifer3/server"
|
lucifer3 "git.aiterp.net/lucifer3/server"
|
||||||
"git.aiterp.net/lucifer3/server/events"
|
"git.aiterp.net/lucifer3/server/events"
|
||||||
"git.aiterp.net/lucifer3/server/services"
|
"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/httpapiv1"
|
||||||
"git.aiterp.net/lucifer3/server/services/hue"
|
"git.aiterp.net/lucifer3/server/services/hue"
|
||||||
"git.aiterp.net/lucifer3/server/services/mysqldb"
|
"git.aiterp.net/lucifer3/server/services/mysqldb"
|
||||||
"git.aiterp.net/lucifer3/server/services/nanoleaf"
|
"git.aiterp.net/lucifer3/server/services/nanoleaf"
|
||||||
"git.aiterp.net/lucifer3/server/services/uistate"
|
"git.aiterp.net/lucifer3/server/services/uistate"
|
||||||
|
"git.aiterp.net/lucifer3/server/services/variables"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
@@ -40,10 +42,11 @@ func main() {
|
|||||||
|
|
||||||
bus.JoinPrivileged(resolver)
|
bus.JoinPrivileged(resolver)
|
||||||
bus.JoinPrivileged(sceneMap)
|
bus.JoinPrivileged(sceneMap)
|
||||||
bus.Join(services.NewEffectEnforcer(resolver, sceneMap))
|
bus.Join(effectenforcer.NewService(resolver, sceneMap))
|
||||||
bus.Join(nanoleaf.NewService())
|
bus.Join(nanoleaf.NewService())
|
||||||
bus.Join(hue.NewService())
|
bus.Join(hue.NewService())
|
||||||
bus.Join(uistate.NewService())
|
bus.Join(uistate.NewService())
|
||||||
|
bus.Join(variables.NewService(resolver))
|
||||||
bus.Join(database)
|
bus.Join(database)
|
||||||
bus.Join(httpAPI)
|
bus.Join(httpAPI)
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -5,8 +5,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type AddAlias struct {
|
type AddAlias struct {
|
||||||
Match string
|
Match string `json:"match"`
|
||||||
Alias string
|
Alias string `json:"alias"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c AddAlias) CommandDescription() string {
|
func (c AddAlias) CommandDescription() string {
|
||||||
@@ -14,8 +14,8 @@ func (c AddAlias) CommandDescription() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RemoveAlias struct {
|
type RemoveAlias struct {
|
||||||
Match string
|
Match string `json:"match"`
|
||||||
Alias string
|
Alias string `json:"alias"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c RemoveAlias) CommandDescription() string {
|
func (c RemoveAlias) CommandDescription() string {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ func (p *Pointer) AddAlias(alias string) (added *string, removed *string) {
|
|||||||
if strings.HasPrefix(alias, "lucifer:name:") {
|
if strings.HasPrefix(alias, "lucifer:name:") {
|
||||||
for i, alias2 := range p.Aliases {
|
for i, alias2 := range p.Aliases {
|
||||||
if strings.HasPrefix(alias2, "lucifer:name:") {
|
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:]...)
|
p.Aliases = append(p.Aliases[:i], p.Aliases[i+1:]...)
|
||||||
removed = gentools.ShallowCopy(&alias2)
|
removed = gentools.ShallowCopy(&alias2)
|
||||||
break
|
break
|
||||||
@@ -48,6 +49,7 @@ func (p *Pointer) RemoveAlias(alias string) bool {
|
|||||||
|
|
||||||
for i, alias2 := range p.Aliases {
|
for i, alias2 := range p.Aliases {
|
||||||
if alias2 == alias {
|
if alias2 == alias {
|
||||||
|
p.Aliases = append(p.Aliases[:0:0], p.Aliases...)
|
||||||
p.Aliases = append(p.Aliases[:i], p.Aliases[i+1:]...)
|
p.Aliases = append(p.Aliases[:i], p.Aliases[i+1:]...)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-4
@@ -70,10 +70,16 @@ func (s State) Interpolate(s2 State, f float64) State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if s.Power != nil && s2.Power != nil {
|
if s.Power != nil && s2.Power != nil {
|
||||||
if f < 0.5 {
|
if *s.Power == *s2.Power {
|
||||||
newState.Power = gentools.ShallowCopy(s.Power)
|
newState.Power = s.Power
|
||||||
} else {
|
}
|
||||||
newState.Power = gentools.ShallowCopy(s2.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 {
|
} else if s.Power != nil {
|
||||||
newState.Power = gentools.ShallowCopy(s.Power)
|
newState.Power = gentools.ShallowCopy(s.Power)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ type serializedEffect struct {
|
|||||||
Gradient *Gradient `json:"gradient,omitempty"`
|
Gradient *Gradient `json:"gradient,omitempty"`
|
||||||
Pattern *Pattern `json:"pattern,omitempty"`
|
Pattern *Pattern `json:"pattern,omitempty"`
|
||||||
Random *Random `json:"random,omitempty"`
|
Random *Random `json:"random,omitempty"`
|
||||||
|
VRange *VRange `json:"vrange,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Serializable struct {
|
type Serializable struct {
|
||||||
@@ -33,6 +34,8 @@ func (s *Serializable) UnmarshalJSON(raw []byte) error {
|
|||||||
s.Effect = *value.Pattern
|
s.Effect = *value.Pattern
|
||||||
case value.Random != nil:
|
case value.Random != nil:
|
||||||
s.Effect = *value.Random
|
s.Effect = *value.Random
|
||||||
|
case value.VRange != nil:
|
||||||
|
s.Effect = *value.VRange
|
||||||
default:
|
default:
|
||||||
return errors.New("unsupported effect")
|
return errors.New("unsupported effect")
|
||||||
}
|
}
|
||||||
@@ -50,6 +53,8 @@ func (s *Serializable) MarshalJSON() ([]byte, error) {
|
|||||||
return json.Marshal(serializedEffect{Pattern: &effect})
|
return json.Marshal(serializedEffect{Pattern: &effect})
|
||||||
case Random:
|
case Random:
|
||||||
return json.Marshal(serializedEffect{Random: &effect})
|
return json.Marshal(serializedEffect{Random: &effect})
|
||||||
|
case VRange:
|
||||||
|
return json.Marshal(serializedEffect{VRange: &effect})
|
||||||
default:
|
default:
|
||||||
panic(s.Effect.EffectDescription() + "is not understood by serializer")
|
panic(s.Effect.EffectDescription() + "is not understood by serializer")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -14,3 +14,10 @@ type Effect interface {
|
|||||||
Frequency() time.Duration
|
Frequency() time.Duration
|
||||||
EffectDescription() string
|
EffectDescription() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VariableEffect interface {
|
||||||
|
Effect
|
||||||
|
|
||||||
|
VariableName() string
|
||||||
|
VariableState(index, len int, value float64) device.State
|
||||||
|
}
|
||||||
|
|||||||
+21
-18
@@ -3,7 +3,9 @@ package color
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.aiterp.net/lucifer3/server/internal/gentools"
|
||||||
"github.com/lucasb-eyer/go-colorful"
|
"github.com/lucasb-eyer/go-colorful"
|
||||||
|
"log"
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -56,10 +58,6 @@ func (col *Color) SetXY(xy XY) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (col *Color) AlmostEquals(other Color) bool {
|
func (col *Color) AlmostEquals(other Color) bool {
|
||||||
if (col.K != nil) != (other.K != nil) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if col.HS != nil && other.HS != nil {
|
if col.HS != nil && other.HS != nil {
|
||||||
if math.Abs(col.HS.Hue-other.HS.Hue) > 0.01 {
|
if math.Abs(col.HS.Hue-other.HS.Hue) > 0.01 {
|
||||||
return false
|
return false
|
||||||
@@ -68,7 +66,7 @@ func (col *Color) AlmostEquals(other Color) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if col.K != nil && *col.K != *other.K {
|
if col.K != nil && other.K != nil && *col.K != *other.K {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +74,10 @@ func (col *Color) AlmostEquals(other Color) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if col.K != nil {
|
if col.K != nil {
|
||||||
|
if other.K != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return *col.K == *other.K
|
return *col.K == *other.K
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,15 +96,8 @@ func (col *Color) AlmostEquals(other Color) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
xy1, _ := col.ToXY()
|
xy1, _ := col.ToXY()
|
||||||
xy2, _ := col.ToXY()
|
xy2, _ := other.ToXY()
|
||||||
if math.Abs(xy1.XY.X-xy2.XY.X) > 0.001 {
|
return math.Abs(xy1.XY.X-xy2.XY.X)+math.Abs(xy1.XY.Y-xy2.XY.Y) < 0.001
|
||||||
return false
|
|
||||||
}
|
|
||||||
if math.Abs(xy1.XY.Y-xy2.XY.Y) > 0.001 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToRGB tries to copy the color to an RGB color. If it's already RGB, it will be plainly copied, but HS
|
// 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 {
|
func (col *Color) Interpolate(other Color, fac float64) Color {
|
||||||
if col.AlmostEquals(other) {
|
if col.AlmostEquals(other) {
|
||||||
|
log.Println("ALMOST EQUAL", col.String(), other.String())
|
||||||
return *col
|
return *col
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special case for kelvin values.
|
// Special case for both kelvin values.
|
||||||
if col.IsKelvin() && other.IsKelvin() {
|
if col.IsKelvin() && other.IsKelvin() {
|
||||||
k1 := *col.K
|
k1 := *col.K
|
||||||
k2 := *col.K
|
k2 := *other.K
|
||||||
k3 := k1 + int(float64(k2-k1)*fac)
|
|
||||||
return Color{K: &k3}
|
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 {
|
if fac < 0.000001 {
|
||||||
@@ -292,6 +292,9 @@ func (col *Color) String() string {
|
|||||||
|
|
||||||
func (col *Color) colorful() colorful.Color {
|
func (col *Color) colorful() colorful.Color {
|
||||||
switch {
|
switch {
|
||||||
|
case col.K != nil:
|
||||||
|
col2, _ := col.ToXY()
|
||||||
|
return col2.colorful()
|
||||||
case col.HS != nil:
|
case col.HS != nil:
|
||||||
return colorful.Hsv(col.HS.Hue, col.HS.Sat, 1)
|
return colorful.Hsv(col.HS.Hue, col.HS.Sat, 1)
|
||||||
case col.RGB != nil:
|
case col.RGB != nil:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package gentools
|
package gentools
|
||||||
|
|
||||||
func CopyMap[K comparable, V any](m map[K]V) map[K]V {
|
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 {
|
for k, v := range m {
|
||||||
m2[k] = v
|
m2[k] = v
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package services
|
package effectenforcer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
lucifer3 "git.aiterp.net/lucifer3/server"
|
lucifer3 "git.aiterp.net/lucifer3/server"
|
||||||
@@ -7,13 +7,15 @@ import (
|
|||||||
"git.aiterp.net/lucifer3/server/events"
|
"git.aiterp.net/lucifer3/server/events"
|
||||||
"git.aiterp.net/lucifer3/server/internal/color"
|
"git.aiterp.net/lucifer3/server/internal/color"
|
||||||
"git.aiterp.net/lucifer3/server/internal/gentools"
|
"git.aiterp.net/lucifer3/server/internal/gentools"
|
||||||
|
"git.aiterp.net/lucifer3/server/services/variables"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewEffectEnforcer(resolver device.Resolver, sceneMap device.SceneMap) lucifer3.ActiveService {
|
func NewService(resolver device.Resolver, sceneMap device.SceneMap) lucifer3.ActiveService {
|
||||||
s := &effectEnforcer{
|
s := &effectEnforcer{
|
||||||
resolver: resolver,
|
resolver: resolver,
|
||||||
sceneMap: sceneMap,
|
sceneMap: sceneMap,
|
||||||
@@ -34,6 +36,7 @@ type effectEnforcer struct {
|
|||||||
|
|
||||||
supportFlags map[string]device.SupportFlags
|
supportFlags map[string]device.SupportFlags
|
||||||
colorFlags map[string]device.ColorFlags
|
colorFlags map[string]device.ColorFlags
|
||||||
|
variables variables.Variables
|
||||||
|
|
||||||
started uint32
|
started uint32
|
||||||
|
|
||||||
@@ -71,6 +74,20 @@ func (s *effectEnforcer) HandleEvent(_ *lucifer3.EventBus, event lucifer3.Event)
|
|||||||
run.due = time.Now()
|
run.due = time.Now()
|
||||||
}
|
}
|
||||||
s.mu.Unlock()
|
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()
|
s.mu.Lock()
|
||||||
// Create a new run
|
// Create a new run
|
||||||
newRun := &effectEnforcerRun{
|
newRun := &effectEnforcerRun{
|
||||||
|
match: command.Match,
|
||||||
id: id,
|
id: id,
|
||||||
due: time.Now(),
|
due: time.Now(),
|
||||||
ids: allowedIDs,
|
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) {
|
func (s *effectEnforcer) runLoop(bus *lucifer3.EventBus) {
|
||||||
deleteList := make([]int, 0, 8)
|
deleteList := make([]int, 0, 8)
|
||||||
batch := make(commands.SetStateBatch, 64)
|
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)
|
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
|
batch[id] = state
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if freq := run.effect.Frequency(); freq > 0 {
|
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) {
|
if state.Color != nil && !sf.HasAny(device.SFlagColor) {
|
||||||
state.Color = nil
|
state.Color = nil
|
||||||
} else {
|
} else if state.Color != nil {
|
||||||
cf := colorFlags[id]
|
cf := colorFlags[id]
|
||||||
invalid := (state.Color.K != nil && !cf.HasAll(device.CFlagKelvin)) ||
|
invalid := (state.Color.K != nil && !cf.HasAll(device.CFlagKelvin)) ||
|
||||||
(state.Color.XY != nil && !cf.HasAll(device.CFlagXY)) ||
|
(state.Color.XY != nil && !cf.HasAll(device.CFlagXY)) ||
|
||||||
@@ -252,6 +313,7 @@ func (s *effectEnforcer) runLoop(bus *lucifer3.EventBus) {
|
|||||||
|
|
||||||
type effectEnforcerRun struct {
|
type effectEnforcerRun struct {
|
||||||
id uuid.UUID
|
id uuid.UUID
|
||||||
|
match string
|
||||||
due time.Time
|
due time.Time
|
||||||
ids []string
|
ids []string
|
||||||
effect lucifer3.Effect
|
effect lucifer3.Effect
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package services
|
package effectenforcer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
lucifer3 "git.aiterp.net/lucifer3/server"
|
lucifer3 "git.aiterp.net/lucifer3/server"
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"git.aiterp.net/lucifer3/server/internal/color"
|
"git.aiterp.net/lucifer3/server/internal/color"
|
||||||
"git.aiterp.net/lucifer3/server/internal/gentools"
|
"git.aiterp.net/lucifer3/server/internal/gentools"
|
||||||
"git.aiterp.net/lucifer3/server/internal/testutils"
|
"git.aiterp.net/lucifer3/server/internal/testutils"
|
||||||
|
"git.aiterp.net/lucifer3/server/services"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -16,13 +17,13 @@ import (
|
|||||||
|
|
||||||
func TestEffectEnforcer(t *testing.T) {
|
func TestEffectEnforcer(t *testing.T) {
|
||||||
bus := lucifer3.EventBus{}
|
bus := lucifer3.EventBus{}
|
||||||
resolver := NewResolver()
|
resolver := services.NewResolver()
|
||||||
sceneMap := NewSceneMap(resolver)
|
sceneMap := services.NewSceneMap(resolver)
|
||||||
logger := &testutils.TestEventLogger{}
|
logger := &testutils.TestEventLogger{}
|
||||||
|
|
||||||
bus.JoinPrivileged(resolver)
|
bus.JoinPrivileged(resolver)
|
||||||
bus.JoinPrivileged(sceneMap)
|
bus.JoinPrivileged(sceneMap)
|
||||||
bus.Join(NewEffectEnforcer(resolver, sceneMap))
|
bus.Join(NewService(resolver, sceneMap))
|
||||||
bus.Join(logger)
|
bus.Join(logger)
|
||||||
|
|
||||||
for i := 1; i <= 9; i += 1 {
|
for i := 1; i <= 9; i += 1 {
|
||||||
@@ -54,6 +54,10 @@ func New(addr string) (lucifer3.Service, error) {
|
|||||||
bus.RunCommand(*input.ForgetDevice)
|
bus.RunCommand(*input.ForgetDevice)
|
||||||
case input.SearchDevices != nil:
|
case input.SearchDevices != nil:
|
||||||
bus.RunCommand(*input.SearchDevices)
|
bus.RunCommand(*input.SearchDevices)
|
||||||
|
case input.AddAlias != nil:
|
||||||
|
bus.RunCommand(*input.AddAlias)
|
||||||
|
case input.RemoveAlias != nil:
|
||||||
|
bus.RunCommand(*input.RemoveAlias)
|
||||||
default:
|
default:
|
||||||
return c.String(400, "No supported command found in input")
|
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 {
|
type commandInput struct {
|
||||||
Assign *assignInput `json:"assign,omitempty"`
|
Assign *assignInput `json:"assign,omitempty"`
|
||||||
|
AddAlias *commands.AddAlias `json:"addAlias,omitempty"`
|
||||||
|
RemoveAlias *commands.RemoveAlias `json:"removeAlias,omitempty"`
|
||||||
PairDevice *commands.PairDevice `json:"pairDevice,omitempty"`
|
PairDevice *commands.PairDevice `json:"pairDevice,omitempty"`
|
||||||
SearchDevices *commands.SearchDevices `json:"searchDevices,omitempty"`
|
SearchDevices *commands.SearchDevices `json:"searchDevices,omitempty"`
|
||||||
ForgetDevice *commands.ForgetDevice `json:"forgetDevice,omitempty"`
|
ForgetDevice *commands.ForgetDevice `json:"forgetDevice,omitempty"`
|
||||||
|
|||||||
+10
-3
@@ -243,7 +243,7 @@ func (b *Bridge) ApplyPatches(date time.Time, resources []ResourceData) (eventLi
|
|||||||
SecondsSince: 0,
|
SecondsSince: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
lastMotionCopy[resource.ID] = time.Now()
|
lastMotionCopy[b.fullId(resource)] = time.Now()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if resource.Button != nil {
|
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")
|
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)
|
go b.makeCongruentLoop(ctx)
|
||||||
|
|
||||||
bus.RunEvents(hwEvents)
|
bus.RunEvents(hwEvents)
|
||||||
@@ -380,8 +387,8 @@ func (b *Bridge) Run(ctx context.Context, bus *lucifer3.EventBus) interface{} {
|
|||||||
|
|
||||||
for id, value := range lastMotion {
|
for id, value := range lastMotion {
|
||||||
since := time.Since(value)
|
since := time.Since(value)
|
||||||
sinceMod := since % time.Minute
|
sinceMod := since % (time.Second * 30)
|
||||||
if since > time.Second*30 && (sinceMod <= time.Second*5) {
|
if sinceMod >= time.Second*27 || sinceMod <= time.Second*3 {
|
||||||
bus.RunEvent(events.MotionSensed{ID: id, SecondsSince: since.Seconds()})
|
bus.RunEvent(events.MotionSensed{ID: id, SecondsSince: since.Seconds()})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ func (b *bridge) runTouchListener(ctx context.Context, bus *lucifer3.EventBus) {
|
|||||||
goto teardownAndRetry
|
goto teardownAndRetry
|
||||||
}
|
}
|
||||||
|
|
||||||
// Discard all data coming over http.
|
// Discard all data.go coming over http.
|
||||||
reqCloser = res.Body
|
reqCloser = res.Body
|
||||||
go io.Copy(ioutil.Discard, res.Body)
|
go io.Copy(ioutil.Discard, res.Body)
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,19 @@ func (r *Resolver) resolve(pattern string) []*device.Pointer {
|
|||||||
return res
|
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 {
|
func (r *Resolver) Resolve(pattern string) []device.Pointer {
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import (
|
|||||||
"git.aiterp.net/lucifer3/server/events"
|
"git.aiterp.net/lucifer3/server/events"
|
||||||
"git.aiterp.net/lucifer3/server/internal/color"
|
"git.aiterp.net/lucifer3/server/internal/color"
|
||||||
"git.aiterp.net/lucifer3/server/internal/gentools"
|
"git.aiterp.net/lucifer3/server/internal/gentools"
|
||||||
|
"git.aiterp.net/lucifer3/server/services/variables"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Data struct {
|
type Data struct {
|
||||||
Devices map[string]Device `json:"devices"`
|
Devices map[string]Device `json:"devices"`
|
||||||
Assignments map[uuid.UUID]Assignment `json:"assignments"`
|
Assignments map[uuid.UUID]Assignment `json:"assignments"`
|
||||||
|
Variables variables.Variables `json:"variables"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Data) WithPatch(patches ...Patch) Data {
|
func (d *Data) WithPatch(patches ...Patch) Data {
|
||||||
@@ -86,6 +88,10 @@ func (d *Data) WithPatch(patches ...Patch) Data {
|
|||||||
newData.Assignments[pa.ID] = pa
|
newData.Assignments[pa.ID] = pa
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if patch.VariableProperty != nil {
|
||||||
|
newData.Variables = newData.Variables.WithPropertyPatch(*patch.VariableProperty)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return newData
|
return newData
|
||||||
@@ -95,6 +101,7 @@ func (d *Data) Copy() Data {
|
|||||||
return Data{
|
return Data{
|
||||||
Devices: gentools.CopyMap(d.Devices),
|
Devices: gentools.CopyMap(d.Devices),
|
||||||
Assignments: gentools.CopyMap(d.Assignments),
|
Assignments: gentools.CopyMap(d.Assignments),
|
||||||
|
Variables: d.Variables,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import (
|
|||||||
"git.aiterp.net/lucifer3/server/effects"
|
"git.aiterp.net/lucifer3/server/effects"
|
||||||
"git.aiterp.net/lucifer3/server/events"
|
"git.aiterp.net/lucifer3/server/events"
|
||||||
"git.aiterp.net/lucifer3/server/internal/color"
|
"git.aiterp.net/lucifer3/server/internal/color"
|
||||||
|
"git.aiterp.net/lucifer3/server/services/variables"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Patch struct {
|
type Patch struct {
|
||||||
Assignment *AssignmentPatch `json:"assignment,omitempty"`
|
Assignment *AssignmentPatch `json:"assignment,omitempty"`
|
||||||
Device *DevicePatch `json:"device,omitempty"`
|
Device *DevicePatch `json:"device,omitempty"`
|
||||||
|
VariableProperty *variables.PropertyPatch `json:"variableProperty,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e Patch) VerboseKey() string {
|
func (e Patch) VerboseKey() string {
|
||||||
@@ -41,6 +43,8 @@ func (e Patch) EventDescription() string {
|
|||||||
}
|
}
|
||||||
} else if e.Assignment != nil {
|
} else if e.Assignment != nil {
|
||||||
return fmt.Sprintf("uistate.Patch(assignment=%s)", e.Assignment.ID)
|
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 {
|
} else {
|
||||||
return "uistate.Patch"
|
return "uistate.Patch"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"git.aiterp.net/lucifer3/server/effects"
|
"git.aiterp.net/lucifer3/server/effects"
|
||||||
"git.aiterp.net/lucifer3/server/events"
|
"git.aiterp.net/lucifer3/server/events"
|
||||||
"git.aiterp.net/lucifer3/server/internal/gentools"
|
"git.aiterp.net/lucifer3/server/internal/gentools"
|
||||||
|
"git.aiterp.net/lucifer3/server/services/variables"
|
||||||
"sync"
|
"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}}}
|
patches = []Patch{{Device: &DevicePatch{ID: command.ID, DesiredState: &command.State}}}
|
||||||
case commands.SetStateBatch:
|
case commands.SetStateBatch:
|
||||||
for id, state := range command {
|
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),
|
Assignment: gentools.ShallowCopy(event.AssignmentID),
|
||||||
ClearAssignment: event.AssignmentID == nil,
|
ClearAssignment: event.AssignmentID == nil,
|
||||||
}})
|
}})
|
||||||
|
case variables.PropertyPatch:
|
||||||
|
patches = append(patches, Patch{VariableProperty: &event})
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(patches) > 0 {
|
if len(patches) > 0 {
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user