scripting system, hue fixes and such
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
package script
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SetVariable struct {
|
||||
Match *string
|
||||
Devices []string
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
func (c SetVariable) CommandDescription() string {
|
||||
switch {
|
||||
case c.Match != nil:
|
||||
return fmt.Sprintf("SetVariable(tag(%s), %s, %s)", *c.Match, c.Key, c.Value)
|
||||
case c.Devices != nil:
|
||||
return fmt.Sprintf("SetVariable(devices(%s), %s, %s)", strings.Join(c.Devices, ", "), c.Key, c.Value)
|
||||
default:
|
||||
return fmt.Sprintf("SetVariable(global, %s, %s)", c.Key, c.Value)
|
||||
}
|
||||
}
|
||||
|
||||
type UpdateScript struct {
|
||||
Name string `json:"name"`
|
||||
Lines []Line `json:"lines"`
|
||||
}
|
||||
|
||||
func (c UpdateScript) CommandDescription() string {
|
||||
return fmt.Sprintf("UpdateScript(%s, [%d lines...])", c.Name, len(c.Lines))
|
||||
}
|
||||
|
||||
type ExecuteScript struct {
|
||||
Name string `json:"name"`
|
||||
Match string `json:"match"`
|
||||
}
|
||||
|
||||
func (c ExecuteScript) CommandDescription() string {
|
||||
return fmt.Sprintf("ExecuteScript(%s, %s)", c.Name, c.Match)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package script
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Condition struct {
|
||||
Scope string `json:"scope"`
|
||||
Key string `json:"key"`
|
||||
Op string `json:"op"`
|
||||
Value string `json:"value,omitempty"`
|
||||
Not bool `json:"not,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Condition) Check(set VariableSet, match string, devices []string) bool {
|
||||
var value string
|
||||
switch c.Scope {
|
||||
case "global":
|
||||
value = set.Global(c.Key)
|
||||
case "match":
|
||||
value = set.Match(match, c.Key)
|
||||
case "devices":
|
||||
value = set.Devices(devices, c.Key)
|
||||
default:
|
||||
return c.Not
|
||||
}
|
||||
|
||||
valueInt, valueIntErr := strconv.ParseInt(value, 10, 64)
|
||||
targetInt, _ := strconv.ParseInt(c.Value, 10, 64)
|
||||
|
||||
switch c.Op {
|
||||
case "eq":
|
||||
return !c.Not == (value == c.Value)
|
||||
case "neq":
|
||||
return !c.Not == (value != c.Value)
|
||||
case "gt":
|
||||
return !c.Not == (valueInt > targetInt)
|
||||
case "lt":
|
||||
return !c.Not == (valueInt < targetInt)
|
||||
case "gte":
|
||||
log.Println(c.Not, valueInt, targetInt)
|
||||
return !c.Not == (valueInt >= targetInt)
|
||||
case "lte":
|
||||
return !c.Not == (valueInt <= targetInt)
|
||||
case "exists":
|
||||
return !c.Not == (value != "")
|
||||
case "contains":
|
||||
return !c.Not == strings.Contains(value, c.Value)
|
||||
case "numerical":
|
||||
return !c.Not == (valueIntErr == nil)
|
||||
default:
|
||||
return c.Not
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package script
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCondition_Check(t *testing.T) {
|
||||
variables := VariableSet{
|
||||
"global": map[string]string{
|
||||
"test": "32",
|
||||
},
|
||||
"devices:dummy:01;dummy:02": map[string]string{
|
||||
"tstxo": "tsyeym",
|
||||
},
|
||||
"match:lucifer:tag:Soverom": map[string]string{
|
||||
"mode": "daytime",
|
||||
"stuff": "54",
|
||||
},
|
||||
}
|
||||
|
||||
table := []struct {
|
||||
L string
|
||||
R bool
|
||||
C Condition
|
||||
M string
|
||||
D []string
|
||||
}{
|
||||
{
|
||||
"global_gte", true,
|
||||
Condition{"global", "test", "gte", "16", false},
|
||||
"", []string{},
|
||||
},
|
||||
{
|
||||
"global_!gte", true,
|
||||
Condition{"global", "test", "gte", "64", true},
|
||||
"", []string{},
|
||||
},
|
||||
{
|
||||
"global_eq", true,
|
||||
Condition{"global", "test", "eq", "32", false},
|
||||
"", []string{},
|
||||
},
|
||||
{
|
||||
"match_exists", true,
|
||||
Condition{"devices", "tstxo", "exists", "", false},
|
||||
"", []string{"dummy:01", "dummy:02"},
|
||||
},
|
||||
{
|
||||
"match_!exists", false,
|
||||
Condition{"devices", "tstxo", "exists", "", true},
|
||||
"", []string{"dummy:01", "dummy:02"},
|
||||
},
|
||||
{
|
||||
"devices_contains", true,
|
||||
Condition{"match", "mode", "contains", "time", false},
|
||||
"lucifer:tag:Soverom", []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, row := range table {
|
||||
t.Run(row.L, func(t *testing.T) {
|
||||
assert.Equal(t, row.R, row.C.Check(variables, row.M, row.D))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package script
|
||||
|
||||
import (
|
||||
"git.aiterp.net/lucifer3/server/effects"
|
||||
)
|
||||
|
||||
type Script struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Lines []Line `json:"lines,omitempty"`
|
||||
}
|
||||
|
||||
type Line struct {
|
||||
If *LineIf `json:"if,omitempty"`
|
||||
Assign *LineAssign `json:"assign,omitempty"`
|
||||
Set *LineSet `json:"set,omitempty"`
|
||||
}
|
||||
|
||||
type LineSet struct {
|
||||
Scope string `json:"scope"`
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type LineAssign struct {
|
||||
Match string `json:"match,omitempty"`
|
||||
Effect effects.Serializable `json:"effect"`
|
||||
}
|
||||
|
||||
type LineIf struct {
|
||||
Condition *Condition `json:"condition,omitempty"`
|
||||
Then []Line `json:"then,omitempty"`
|
||||
Else []Line `json:"else,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package script
|
||||
|
||||
import (
|
||||
lucifer3 "git.aiterp.net/lucifer3/server"
|
||||
"git.aiterp.net/lucifer3/server/commands"
|
||||
"git.aiterp.net/lucifer3/server/device"
|
||||
"git.aiterp.net/lucifer3/server/internal/formattools"
|
||||
"git.aiterp.net/lucifer3/server/internal/gentools"
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type service struct {
|
||||
mu sync.Mutex
|
||||
resolver device.Resolver
|
||||
variables *Variables
|
||||
scripts map[string]*Script
|
||||
}
|
||||
|
||||
func (s *service) Active() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *service) HandleEvent(_ *lucifer3.EventBus, _ lucifer3.Event) {}
|
||||
|
||||
func (s *service) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Command) {
|
||||
switch command := command.(type) {
|
||||
case UpdateScript:
|
||||
s.mu.Lock()
|
||||
s.scripts[command.Name] = &Script{
|
||||
Name: command.Name,
|
||||
Lines: command.Lines,
|
||||
}
|
||||
s.mu.Unlock()
|
||||
case ExecuteScript:
|
||||
s.mu.Lock()
|
||||
script := s.scripts[command.Name]
|
||||
s.mu.Unlock()
|
||||
if script == nil {
|
||||
break
|
||||
}
|
||||
|
||||
variables := s.variables.Get()
|
||||
|
||||
devices := s.resolver.Resolve(command.Match)
|
||||
sort.Slice(devices, func(i, j int) bool {
|
||||
return devices[i].ID < devices[j].ID
|
||||
})
|
||||
|
||||
s.runScript(bus, script.Lines, command.Match, map[string]bool{}, devices, variables)
|
||||
}
|
||||
}
|
||||
|
||||
func NewService(resolver device.Resolver, variables *Variables) lucifer3.Service {
|
||||
return &service{
|
||||
resolver: resolver,
|
||||
variables: variables,
|
||||
scripts: map[string]*Script{},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) runScript(bus *lucifer3.EventBus, lines []Line, match string, matchedDevices map[string]bool, devices []device.Pointer, variables VariableSet) {
|
||||
for _, line := range lines {
|
||||
if line.If != nil {
|
||||
devicesIDs := gentools.Map(devices, func(d device.Pointer) string { return d.ID })
|
||||
if line.If.Condition.Check(variables, match, devicesIDs) {
|
||||
s.runScript(bus, line.If.Then, match, matchedDevices, devices, variables)
|
||||
} else {
|
||||
s.runScript(bus, line.If.Else, match, matchedDevices, devices, variables)
|
||||
}
|
||||
} else if line.Assign != nil {
|
||||
matcher := s.resolver.CompileMatcher(line.Assign.Match)
|
||||
|
||||
matched := make([]string, 0)
|
||||
for _, dev := range devices {
|
||||
if matchedDevices[dev.ID] {
|
||||
continue
|
||||
}
|
||||
|
||||
if dev.Matches(matcher) {
|
||||
matchedDevices[dev.ID] = true
|
||||
matched = append(matched, dev.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(matched) > 0 {
|
||||
bus.RunCommand(commands.Assign{
|
||||
Match: formattools.CompactMatch(matched),
|
||||
Effect: line.Assign.Effect.Effect,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if line.Set != nil {
|
||||
switch line.Set.Scope {
|
||||
case "devices":
|
||||
bus.RunCommand(SetVariable{
|
||||
Devices: gentools.Map(devices, func(d device.Pointer) string {
|
||||
return d.ID
|
||||
}),
|
||||
Key: line.Set.Key,
|
||||
Value: line.Set.Value,
|
||||
})
|
||||
case "match":
|
||||
bus.RunCommand(SetVariable{
|
||||
Match: gentools.Ptr(match),
|
||||
Key: line.Set.Key,
|
||||
Value: line.Set.Value,
|
||||
})
|
||||
case "global":
|
||||
bus.RunCommand(SetVariable{
|
||||
Key: line.Set.Key,
|
||||
Value: line.Set.Value,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package script
|
||||
|
||||
import (
|
||||
lucifer3 "git.aiterp.net/lucifer3/server"
|
||||
"git.aiterp.net/lucifer3/server/internal/gentools"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type VariableSet map[string]map[string]string
|
||||
|
||||
func (v VariableSet) Global(key string) string {
|
||||
return v["global"][key]
|
||||
}
|
||||
|
||||
func (v VariableSet) Match(tag string, key string) string {
|
||||
return v["match:"+tag][key]
|
||||
}
|
||||
|
||||
func (v VariableSet) Devices(list []string, key string) string {
|
||||
return v["devices:"+strings.Join(list, ";")][key]
|
||||
}
|
||||
|
||||
type Variables struct {
|
||||
mu sync.Mutex
|
||||
vars VariableSet
|
||||
}
|
||||
|
||||
func NewVariables() *Variables {
|
||||
return &Variables{
|
||||
vars: make(VariableSet, 8),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Variables) Get() VariableSet {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
return s.vars
|
||||
}
|
||||
|
||||
func (s *Variables) Active() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Variables) HandleEvent(_ *lucifer3.EventBus, _ lucifer3.Event) {}
|
||||
|
||||
func (s *Variables) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Command) {
|
||||
if sv, ok := command.(SetVariable); ok {
|
||||
s.mu.Lock()
|
||||
curr := gentools.CopyMap(s.vars)
|
||||
s.mu.Unlock()
|
||||
|
||||
key := "global"
|
||||
if sv.Match != nil {
|
||||
key = "match:" + *sv.Match
|
||||
} else if sv.Devices != nil {
|
||||
key = "devices:" + strings.Join(sv.Devices, ";")
|
||||
}
|
||||
|
||||
curr[key] = gentools.CopyMap(curr["global"])
|
||||
|
||||
if sv.Value == "" {
|
||||
delete(curr[key], sv.Key)
|
||||
} else {
|
||||
curr[key][sv.Key] = sv.Value
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.vars = curr
|
||||
s.mu.Unlock()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user