diff --git a/cmd/bustest/main.go b/cmd/bustest/main.go index bd4ebdd..df9e57e 100644 --- a/cmd/bustest/main.go +++ b/cmd/bustest/main.go @@ -6,16 +6,16 @@ import ( "git.aiterp.net/lucifer3/server/commands" "git.aiterp.net/lucifer3/server/device" "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" + "log" "time" ) func main() { bus := lucifer3.EventBus{} - bus.Join(services.Resolver()) + resolver := services.NewResolver() + bus.Join(resolver) bus.RunEvent(events.Connected{Prefix: "nanoleaf:10.80.1.11"}) @@ -32,24 +32,25 @@ func main() { } bus.RunCommand(commands.ReplaceScene{ - IDs: []string{"lucifer:name:Hexagon*"}, + Match: "lucifer:name:Hex*", SceneID: 7, }) - time.Sleep(time.Second / 4) + time.Sleep(time.Second / 8) - for _, id := range []string{"nanoleaf:10.80.1.11:e28c", "nanoleaf:10.80.1.11:67db", "nanoleaf:10.80.1.11:f744", "nanoleaf:10.80.1.11:d057", "nanoleaf:10.80.1.11:73c1"} { - bus.RunCommand(commands.SetState{ - ID: id, - State: device.State{ - Power: gentools.Ptr(true), - Intensity: gentools.Ptr(0.75), - Color: gentools.Ptr(color.MustParse("xy:0.3209,0.1542")), - }, - }) - } + bus.RunCommand(commands.AddAlias{ + Match: "nanoleaf:10.80.1.{11,7,16,5}:*", + Alias: "lucifer:tag:Magic Lamps", + }) - time.Sleep(time.Second / 2) + time.Sleep(time.Second / 8) + + bus.RunCommand(commands.ReplaceScene{ + Match: "lucifer:name:Hex*", + SceneID: 7, + }) + + time.Sleep(time.Second / 8) bus.RunEvent(events.HardwareState{ ID: "nanoleaf:10.80.1.11:40e5", @@ -58,21 +59,14 @@ func main() { ColorFlags: device.CFlagRGB, State: device.State{}, }) - bus.RunCommand(commands.ReplaceScene{ - IDs: []string{"lucifer:name:Hexagon*"}, - SceneID: 7, - }) - time.Sleep(time.Second / 4) + time.Sleep(time.Second / 8) - bus.RunCommand(commands.SetState{ - ID: "nanoleaf:10.80.1.11:40e5", - State: device.State{ - Power: gentools.Ptr(true), - Intensity: gentools.Ptr(0.75), - Color: gentools.Ptr(color.MustParse("xy:0.3209,0.1542")), - }, - }) + log.Println("Search \"**:Hexagon {1,5,6}\"") + for _, dev := range resolver.Resolve("lucifer:name:Hexagon {1,5,6}") { + log.Println("- ID:", dev.ID) + log.Println(" Aliases:", dev.Aliases) + } time.Sleep(time.Second / 4) } diff --git a/commands/edit.go b/commands/edit.go index a4ecf01..56baf45 100644 --- a/commands/edit.go +++ b/commands/edit.go @@ -2,32 +2,22 @@ package commands import ( "fmt" - "git.aiterp.net/lucifer3/server/internal/formattools" ) -type SetName struct { - ID string - Name string +type AddAlias struct { + Match string + Alias string } -func (c SetName) CommandDescription() string { - return fmt.Sprintf("SetName(%v, %s)", c.ID, c.Name) +func (c AddAlias) CommandDescription() string { + return fmt.Sprintf("AddAlias(%v, %v)", c.Match, c.Alias) } -type AddTag struct { - IDs []string - Tag string +type RemoveAlias struct { + Match string + Alias string } -func (c AddTag) CommandDescription() string { - return fmt.Sprintf("AddTag(%v, %s)", formattools.CompactIDList(c.IDs), c.Tag) -} - -type RemoveTag struct { - IDs []string - Tag string -} - -func (c RemoveTag) CommandDescription() string { - return fmt.Sprintf("RemoveTag(%v, %s)", formattools.CompactIDList(c.IDs), c.Tag) +func (c RemoveAlias) CommandDescription() string { + return fmt.Sprintf("RemoveAlias(%v, %v)", c.Match, c.Alias) } diff --git a/commands/scene.go b/commands/scene.go index 66bdc4f..70ba6c6 100644 --- a/commands/scene.go +++ b/commands/scene.go @@ -2,22 +2,21 @@ package commands import ( "fmt" - "git.aiterp.net/lucifer3/server/internal/formattools" ) type ReplaceScene struct { - IDs []string `json:"ids"` - SceneID int64 `json:"sceneId"` + Match string `json:"match"` + SceneID int64 `json:"sceneId"` } func (c ReplaceScene) CommandDescription() string { - return fmt.Sprintf("ReplaceScene(%v, %d)", formattools.CompactIDList(c.IDs), c.SceneID) + return fmt.Sprintf("ReplaceScene(%v, %d)", c.Match, c.SceneID) } type ClearScene struct { - IDs []string `json:"ids"` + Match string `json:"match"` } func (c ClearScene) CommandDescription() string { - return fmt.Sprintf("ClearScene(%v)", formattools.CompactIDList(c.IDs)) + return fmt.Sprintf("ClearScene(%v)", c.Match) } diff --git a/device/Resolver.go b/device/Resolver.go new file mode 100644 index 0000000..ec40bfe --- /dev/null +++ b/device/Resolver.go @@ -0,0 +1,5 @@ +package device + +type Resolver interface { + Resolve(pattern string) []Pointer +} diff --git a/device/pointer.go b/device/pointer.go new file mode 100644 index 0000000..73a5011 --- /dev/null +++ b/device/pointer.go @@ -0,0 +1,65 @@ +package device + +import ( + "strings" +) + +type PointerMatcher interface { + Match(str string) bool +} + +type Pointer struct { + ID string `json:"id"` + Aliases []string `json:"aliases"` +} + +func (p *Pointer) AddAlias(alias string) { + if strings.HasPrefix(alias, "lucifer:name:") { + for i, alias2 := range p.Aliases { + if strings.HasPrefix(alias2, "lucifer:name:") { + p.Aliases = append(p.Aliases[:i], p.Aliases[i+1:]...) + break + } + } + } + + for _, alias2 := range p.Aliases { + if alias2 == alias { + return + } + } + + p.Aliases = append(p.Aliases, alias) +} + +func (p *Pointer) RemoveAlias(alias string) { + for i, alias2 := range p.Aliases { + if alias2 == alias { + p.Aliases = append(p.Aliases[:i], p.Aliases[i+1:]...) + break + } + } +} + +func (p *Pointer) Name() string { + for _, alias := range p.Aliases { + if strings.HasPrefix(alias, "lucifer:name:") { + return alias + } + } + + return "" +} + +func (p *Pointer) Matches(matcher PointerMatcher) bool { + if matcher.Match(p.ID) { + return true + } + for _, alias := range p.Aliases { + if matcher.Match(alias) { + return true + } + } + + return false +} diff --git a/go.mod b/go.mod index ecc638a..4ee582c 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,5 @@ module git.aiterp.net/lucifer3/server go 1.19 require github.com/lucasb-eyer/go-colorful v1.2.0 + +require github.com/gobwas/glob v0.2.3 // indirect diff --git a/go.sum b/go.sum index eae3068..ffef3ce 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= diff --git a/services/resolver.go b/services/resolver.go index de4af63..d35c4a7 100644 --- a/services/resolver.go +++ b/services/resolver.go @@ -3,143 +3,105 @@ package services import ( lucifer3 "git.aiterp.net/lucifer3/server" "git.aiterp.net/lucifer3/server/commands" + "git.aiterp.net/lucifer3/server/device" "git.aiterp.net/lucifer3/server/events" - "git.aiterp.net/lucifer3/server/internal/gentools" - "reflect" - "strings" + "github.com/gobwas/glob" + "sync" ) -func Resolver() lucifer3.Service { - return &resolver{ - tags: make(map[string][]string), - names: make(map[string][]string), +func NewResolver() *Resolver { + return &Resolver{ + pointers: make([]*device.Pointer, 0, 128), } } -type resolver struct { - tags map[string][]string - names map[string][]string +// Resolver implements both device.Resolver and lucifer3.Service. There should be only one of them as it keeps its +// state from event-sourcery. +type Resolver struct { + mu sync.Mutex + pointers []*device.Pointer } -func (r *resolver) Active() bool { - return true +func (r *Resolver) ensure(id string) *device.Pointer { + for _, ptr := range r.pointers { + if ptr.ID == id { + return ptr + } + } + + r.pointers = append(r.pointers, &device.Pointer{ + ID: id, + }) + + return r.pointers[len(r.pointers)-1] } -func (r *resolver) HandleEvent(_ *lucifer3.EventBus, event lucifer3.Event) { - switch event := event.(type) { - case events.HardwareState: - // On HardwareState, use the internal name if (and only if) it is not already named. - - if event.InternalName != "" { - found := false - for _, ids := range r.names { - i := gentools.IndexOf(ids, event.ID) - if i != -1 { - found = true - break - } - } - - if !found { - r.names[event.InternalName] = append(r.names[event.InternalName], event.ID) - } +func (r *Resolver) resolve(pattern string) []*device.Pointer { + g, err := glob.Compile(pattern, ':') + if err != nil { + return []*device.Pointer{} + } + + res := make([]*device.Pointer, 0, 32) + for _, ptr := range r.pointers { + if ptr.Matches(g) { + res = append(res, ptr) } } + + return res } -func (r *resolver) rerun(bus *lucifer3.EventBus, resolveList *[]string, command interface{}) { - newList := make([]string, 0, 16) - - for _, id := range *resolveList { - switch { - case strings.HasPrefix(id, "lucifer:tag:"): - tag := id[12:] - if len(newList) == 0 { - newList = append(newList, r.tags[tag]...) - } else { - gentools.AddUniques(&newList, r.tags[tag]...) - } - case strings.HasPrefix(id, "lucifer:name:"): - name := id[13:] - if strings.HasSuffix(name, "*") { - prefix := name[:len(name)-1] - for name, ids := range r.names { - if strings.HasPrefix(name, prefix) { - gentools.AddUniques(&newList, ids...) - } - } - } else if strings.HasPrefix(name, "*") { - suffix := name[1:] - for name, ids := range r.names { - if strings.HasSuffix(name, suffix) { - gentools.AddUniques(&newList, ids...) - } - } - } else { - gentools.AddUniques(&newList, r.names[name]...) - } - } +func (r *Resolver) Resolve(pattern string) []device.Pointer { + g, err := glob.Compile(pattern, ':') + if err != nil { + return []device.Pointer{} } - if len(newList) > 0 { - *resolveList = newList + r.mu.Lock() + defer r.mu.Unlock() - bus.RunCommand(reflect.Indirect(reflect.ValueOf(command)).Interface().(lucifer3.Command)) + res := make([]device.Pointer, 0, 32) + for _, ptr := range r.pointers { + if ptr.Matches(g) { + res = append(res, *ptr) + } } + + return res } -func (r *resolver) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Command) { +func (r *Resolver) Active() bool { + return true +} - switch command := command.(type) { - case commands.Assign: - r.rerun(bus, &command.IDs, &command) - case commands.ReplaceScene: - r.rerun(bus, &command.IDs, &command) - case commands.ClearScene: - r.rerun(bus, &command.IDs, &command) - case commands.SetName: - if !strings.HasPrefix(command.ID, "lucifer:") { - for name := range r.names { - i := gentools.IndexOf(r.names[name], command.ID) - if i != -1 { - r.names[name] = append(r.names[name][:i], r.names[name][i+1:]...) - } - } - r.names[command.Name] = append(r.names[command.Name], command.ID) - } - case commands.AddTag: - for _, id := range command.IDs { - if strings.HasPrefix(id, "lucifer:") { - continue - } - - found := false - for _, id2 := range r.tags[command.Tag] { - if id == id2 { - found = true - break - } - } - if !found { - r.tags[command.Tag] = append(r.tags[command.Tag], id) - } +func (r *Resolver) HandleEvent(_ *lucifer3.EventBus, event lucifer3.Event) { + switch event := event.(type) { + // On HardwareState, use the internal name if (and only if) it is not already named. + case events.HardwareState: + r.mu.Lock() + ptr := r.ensure(event.ID) + if event.InternalName != "" && ptr.Name() == "" { + ptr.AddAlias("lucifer:name:" + event.InternalName) } + r.mu.Unlock() + } +} - r.rerun(bus, &command.IDs, &command) - case commands.RemoveTag: - for _, id := range command.IDs { - if strings.HasPrefix(id, "lucifer:") { - continue - } - - for i, id2 := range r.tags[command.Tag] { - if id == id2 { - r.tags[command.Tag] = append(r.tags[command.Tag][:i], r.tags[command.Tag][i+1:]...) - break - } - } +func (r *Resolver) HandleCommand(_ *lucifer3.EventBus, command lucifer3.Command) { + switch command := command.(type) { + case commands.AddAlias: + r.mu.Lock() + for _, ptr := range r.resolve(command.Match) { + ptr.AddAlias(command.Alias) } + r.mu.Unlock() - r.rerun(bus, &command.IDs, &command) + case commands.RemoveAlias: + r.mu.Lock() + for _, ptr := range r.resolve(command.Match) { + ptr.AddAlias(command.Alias) + } + r.mu.Unlock() } }