Compare commits

...

2 Commits
master ... dev

Author SHA1 Message Date
7aef6194d1
further development 2022-09-05 05:07:00 -07:00
eb95d06a1b
further development 2022-09-05 05:06:33 -07:00
6 changed files with 321 additions and 147 deletions

1
.gitignore vendored

@ -3,3 +3,4 @@ assets/
*.unused
*.toml
*.swp
*.save

@ -5,6 +5,7 @@ import (
"fmt"
"os"
"strings"
"sync"
"time"
cli "git.tcp.direct/Mirrors/go-prompt"
@ -18,7 +19,10 @@ import (
"git.tcp.direct/kayos/ziggs/internal/ziggy"
)
var log *zerolog.Logger
var (
log *zerolog.Logger
prompt *cli.Prompt
)
func validate(input string) error {
if len(strings.TrimSpace(input)) < 1 {
@ -27,6 +31,59 @@ func validate(input string) error {
return nil
}
type Selection struct {
Bridge string
Action string
Target struct {
Type string
Name string
}
}
type pool struct {
p *sync.Pool
}
var stringers = pool{p: &sync.Pool{
New: func() interface{} {
return &strings.Builder{}
}}}
func (p *pool) Get() *strings.Builder {
return p.p.Get().(*strings.Builder)
}
func (p *pool) Put(sb *strings.Builder) {
sb.Reset()
p.p.Put(sb)
}
func (s *Selection) String() string {
if s.Bridge == "" && s.Action == "" {
return "~"
}
builder := stringers.Get()
builder.WriteString(s.Bridge)
if s.Action != "" {
builder.WriteString("/")
builder.WriteString(s.Action)
}
if s.Target.Type != "" {
builder.WriteString("/")
builder.WriteString(s.Target.Type)
builder.WriteString("s")
}
if s.Target.Name != "" {
builder.WriteString("/")
builder.WriteString(s.Target.Name)
}
res := builder.String()
stringers.Put(builder)
return res
}
var sel = &Selection{}
func InteractiveAuth() string {
passPrompt := tui.Prompt{
Label: "API Key (AKA user)",
@ -56,6 +113,9 @@ func executor(cmd string) {
case "quit", "exit":
os.Exit(0)
case "use":
if len(ziggy.Lucifer.Bridges) < 2 {
return
}
if len(args) < 2 {
println("use: use <bridge>")
return
@ -63,8 +123,8 @@ func executor(cmd string) {
if br, ok := ziggy.Lucifer.Bridges[args[1]]; !ok {
println("invalid bridge: " + args[1])
} else {
selectedBridge = args[1]
log.Info().Str("host", br.Host).Int("lights", len(br.HueLights)).Msg("switched to bridge: " + selectedBridge)
sel.Bridge = args[1]
log.Info().Str("host", br.Host).Int("lights", len(br.HueLights)).Msg("switched to bridge: " + sel.Bridge)
}
case "debug":
levelsdebug := map[string]zerolog.Level{"info": zerolog.InfoLevel, "debug": zerolog.DebugLevel, "trace": zerolog.TraceLevel}
@ -93,14 +153,14 @@ func executor(cmd string) {
if !ok {
return
}
br, ok := ziggy.Lucifer.Bridges[selectedBridge]
if selectedBridge == "" || !ok {
prompt := tui.Select{
br, ok := ziggy.Lucifer.Bridges[sel.Bridge]
if sel.Bridge == "" || !ok {
q := tui.Select{
Label: "Send to all known bridges?",
Items: []string{"yes", "no"},
Pointer: common.ZiggsPointer,
}
_, ch, _ := prompt.Run()
_, ch, _ := q.Run()
if ch != "yes" {
return
}
@ -182,8 +242,6 @@ loop:
return nil
}
var selectedBridge = ""
const bulb = ``
func getHist() []string {
@ -192,17 +250,14 @@ func getHist() []string {
func StartCLI() {
log = config.GetLogger()
processBridges(ziggy.Lucifer.Bridges)
for _, br := range ziggy.Lucifer.Bridges {
grpmap, err := getGroupMap(br)
if err != nil {
log.Warn().Err(err).Msg("error getting group map")
} else {
processGroups(br, grpmap)
}
processBridges()
grpmap, err := getGroupMap()
if err != nil {
log.Fatal().Err(err).Msg("error getting group map")
}
p := cli.New(
processGroups(grpmap)
processLights()
prompt = cli.New(
executor,
completer,
// cli.OptionPrefixBackgroundColor(cli.Black),
@ -214,15 +269,16 @@ func StartCLI() {
cli.OptionSelectedSuggestionTextColor(cli.Green),
cli.OptionLivePrefix(
func() (prefix string, useLivePrefix bool) {
sel := "~"
if len(ziggy.Lucifer.Bridges) > 1 && selectedBridge != "" {
sel = selectedBridge
if len(ziggy.Lucifer.Bridges) == 1 {
for brid, _ := range ziggy.Lucifer.Bridges {
sel.Bridge = brid
}
}
return fmt.Sprintf("ziggs[%s] %s ", sel, bulb), true
return fmt.Sprintf("ziggs[%s] %s ", sel.String(), bulb), true
}),
cli.OptionTitle("ziggs"),
cli.OptionCompletionOnDown(),
)
p.Run()
prompt.Run()
}

@ -8,8 +8,8 @@ import (
"strconv"
"time"
cli "git.tcp.direct/Mirrors/go-prompt"
"github.com/amimof/huego"
"github.com/davecgh/go-spew/spew"
"git.tcp.direct/kayos/ziggs/internal/system"
"git.tcp.direct/kayos/ziggs/internal/ziggy"
@ -23,10 +23,6 @@ var (
cpuCancel context.CancelFunc
)
func init() {
cpuCtx, cpuCancel = context.WithCancel(context.Background())
}
func ParseHexColorFast(s string) (c color.RGBA, err error) {
c.A = 0xff
@ -74,30 +70,35 @@ func cmdLights(br *ziggy.Bridge, args []string) error {
return nil
}
type cmdTarget interface {
On() error
Off() error
Bri(uint8) error
Ct(uint16) error
Hue(uint16) error
Sat(uint8) error
Col(color.Color) error
SetState(huego.State) error
Alert(string) error
}
func cmdSet(bridge *ziggy.Bridge, args []string) error {
if len(args) < 3 {
return errors.New("not enough arguments")
}
var target interface {
On() error
Off() error
Bri(uint8) error
Ct(uint16) error
Hue(uint16) error
Sat(uint8) error
Col(color.Color) error
SetState(huego.State) error
Alert(string) error
}
type (
action func() error
)
var groupmap map[string]*huego.Group
var (
groupmap map[string]*huego.Group
actions []action
currentState *huego.State
argHead = -1
target cmdTarget
)
type action func() error
var actions []action
var currentState *huego.State
var argHead = -1
for range args {
argHead++
if len(args) <= argHead {
@ -107,7 +108,7 @@ func cmdSet(bridge *ziggy.Bridge, args []string) error {
switch args[argHead] {
case "group", "g", "grp":
var err error
groupmap, err = getGroupMap(bridge)
groupmap, err = getGroupMap()
if err != nil {
return err
}
@ -119,6 +120,7 @@ func cmdSet(bridge *ziggy.Bridge, args []string) error {
if !ok {
return errors.New("group not found")
}
target = g
case "on":
actions = append(actions, target.On)
@ -189,34 +191,32 @@ func cmdSet(bridge *ziggy.Bridge, args []string) error {
case "cpu":
switch cpuOn {
case false:
cpuCtx, cpuCancel = context.WithCancel(context.Background())
load, err := system.CPULoadGradient(cpuCtx,
"deepskyblue", "seagreen", "darkorchid", "gold", "deeppink")
if err != nil {
return err
}
log.Info().Msg("turning CPU load lights on")
go func() {
log.Info().Msg("turning CPU load lights on for ")
go func(cpuTarget cmdTarget) {
cpuOn = true
defer func() {
cpuOn = false
}()
cpuTarget := target
for {
select {
case <-cpuCtx.Done():
cpuOn = false
return
default:
time.Sleep(2 * time.Second)
clr := <-load
case clr := <-load:
log.Trace().Msgf("CPU load color: %v", clr.Hex())
cHex, cErr := ParseHexColorFast(clr.Hex())
if cErr != nil {
log.Error().Err(cErr).Msg("failed to parse color")
continue
}
colErr := cpuTarget.Col(cHex)
_ = cpuTarget.Bri(100)
if colErr != nil {
log.Error().Err(colErr).Msg("failed to set color")
time.Sleep(3 * time.Second)
@ -224,7 +224,7 @@ func cmdSet(bridge *ziggy.Bridge, args []string) error {
}
}
}
}()
}(target)
return nil
case true:
log.Info().Msg("turning CPU load lights off")
@ -241,13 +241,15 @@ func cmdSet(bridge *ziggy.Bridge, args []string) error {
if target == nil {
return errors.New("no target specified")
}
tg, tgok := target.(*huego.Group)
tl, tlok := target.(*huego.Light)
tgroup, tgok := target.(*huego.Group)
tlight, tlok := target.(*huego.Light)
switch {
case tgok:
currentState = tg.State
currentState = tgroup.State
case tlok:
currentState = tl.State
currentState = tlight.State
default:
return errors.New("unknown target")
}
log.Trace().Msgf("current state: %v", currentState)
for d, act := range actions {
@ -258,84 +260,61 @@ func cmdSet(bridge *ziggy.Bridge, args []string) error {
}
switch {
case tgok:
currentState = tg.State
currentState = tgroup.State
case tlok:
currentState = tl.State
currentState = tlight.State
}
log.Trace().Msgf("new state: %v", currentState)
}
return nil
}
func getGroupMap(br *ziggy.Bridge) (map[string]*huego.Group, error) {
func getGroupMap() (map[string]*huego.Group, error) {
var groupmap = make(map[string]*huego.Group)
gs, err := br.Bridge.GetGroups()
if err != nil {
return nil, err
}
for _, g := range gs {
groupmap[g.Name] = &g
for _, br := range ziggy.Lucifer.Bridges {
groups, err := br.GetGroups()
log.Trace().Msgf(spew.Sprint(groups))
if err != nil {
return nil, err
}
for _, group := range groups {
groupName := group.Name
var count = 1
for _, ok := groupmap[groupName]; ok; _, ok = groupmap[groupName] {
groupName = fmt.Sprintf("%s_%d", group.Name, count)
}
groupmap[groupName] = &group
}
}
return groupmap, nil
}
func getLightMap(br *ziggy.Bridge) (map[string]*huego.Light, error) {
var lightmap = make(map[string]*huego.Light)
ls, err := br.Bridge.GetLights()
if err != nil {
return nil, err
}
for _, l := range ls {
lightmap[l.Name] = &l
}
return lightmap, nil
}
func cmdGroups(br *ziggy.Bridge, args []string) error {
groupmap, err := getGroupMap(br)
groupmap, err := getGroupMap()
if err != nil {
return err
}
if len(groupmap) == 0 {
return errors.New("no groups found")
}
for _, g := range groupmap {
log.Info().Str("caller", g.Name).Str("type", g.Type).Int("ID", g.ID).
Str("class", g.Class).Bool("on", g.IsOn()).Msgf("%v", g.GroupState)
for n, g := range groupmap {
if n != g.Name {
log.Warn().Msgf("group name mismatch: %s != %s", n, g.Name)
}
slog := log.With().Str("caller", g.Name).Int("ID", g.ID).Logger()
slog.Info().Msgf("\n\tType: %v\n\tClass: %v\n\t%v", g.Type, g.Class, spew.Sprint(g.State))
}
return nil
}
type reactor func(bridge *ziggy.Bridge, args []string) error
var bridgeCMD = map[string]reactor{
"scan": cmdScan,
"lights": cmdLights,
"groups": cmdGroups,
"set": cmdSet,
}
type completeMapper map[*cli.Suggest][]cli.Suggest
var suggestions completeMapper = make(map[*cli.Suggest][]cli.Suggest)
func processGroups(br *ziggy.Bridge, grps map[string]*huego.Group) {
set := &cli.Suggest{
Text: "set",
}
suggestions[set] = []cli.Suggest{
{Text: "light"},
}
/* for grp, g := range grps {
suggestions[set] = append(
suggestions[set],
cli.Suggest{
Text: grp,
Description: br.ID + ": " + g.Type,
})
}*/
}
func processBridges(brs map[string]*ziggy.Bridge) {
for brd, c := range brs {
use := cli.Suggest{
Text: "use",
Description: "select bridge to perform actions on",
}
suggestions[&use] = append(
suggestions[&use],
cli.Suggest{
Text: brd,
Description: c.Host,
})
}
}

@ -1,39 +1,172 @@
package interactive
import (
"strings"
cli "git.tcp.direct/Mirrors/go-prompt"
"github.com/amimof/huego"
"git.tcp.direct/kayos/ziggs/internal/ziggy"
)
type reactor func(bridge *ziggy.Bridge, args []string) error
const (
grn = "\033[32m"
red = "\033[31m"
ylw = "\033[33m"
rst = "\033[0m"
)
var bridgeCMD = map[string]reactor{
"scan": cmdScan,
"lights": cmdLights,
"groups": cmdGroups,
"set": cmdSet,
}
type completion struct {
cli.Suggest
requires map[int][]string
root bool
}
func (c completion) qualifies(line string) bool {
args := strings.Fields(line)
if c.root && len(args) < 1 {
return true
}
/*if c.root && len(args) > 0 {
return false
}*/
if len(args) < len(c.requires) {
log.Trace().Int("len(args)", len(args)).Int("len(c.requires)", len(c.requires)).
Msg(red + "len(args) < len(c.requires)" + rst)
return false
}
if len(args)-2 > len(c.requires) {
log.Trace().Int("len(args)-2", len(args)-2).Int("len(c.requires)", len(c.requires)).
Msg(red + "len(args)-2 > len(c.requires)" + rst)
return false
}
has := func(b []string, a string) bool {
for _, r := range b {
if a == r {
return true
}
}
return false
}
var count = 0
for i, a := range args {
if has(c.requires[i], a) {
log.Trace().Msgf("%v%s: found %s%v", grn, c.Text, a, rst)
count++
}
}
if count == len(c.requires) {
return true
}
return false
}
var suggestions map[int][]completion
func init() {
suggestions = make(map[int][]completion)
suggestions[0] = []completion{
{Suggest: cli.Suggest{Text: "lights", Description: "print all known lights"}},
{Suggest: cli.Suggest{Text: "groups", Description: "print all known groups"}},
{Suggest: cli.Suggest{Text: "clear", Description: "clear screen"}},
{Suggest: cli.Suggest{Text: "scan", Description: "scan for bridges"}},
{Suggest: cli.Suggest{Text: "exit", Description: "exit ziggs"}},
{Suggest: cli.Suggest{Text: "quit", Description: "exit ziggs"}},
{Suggest: cli.Suggest{Text: "set", Description: "set state of target"}},
{Suggest: cli.Suggest{Text: "use", Description: "select bridge to perform actions on"}},
}
for _, sug := range suggestions[0] {
sug.requires = map[int][]string{}
sug.root = true
}
suggestions[1] = []completion{
{Suggest: cli.Suggest{Text: "group", Description: "target group"}},
{Suggest: cli.Suggest{Text: "light", Description: "target light"}},
}
for _, sug := range suggestions[1] {
sug.requires = map[int][]string{0: {"set", "s"}}
sug.root = false
}
}
func processGroups(grps map[string]*huego.Group) {
for grp, g := range grps {
suffix := ""
if g.Type != "" {
suffix = " (" + g.Type + ")"
}
suggestions[2] = append(suggestions[2],
completion{
Suggest: cli.Suggest{
Text: grp,
Description: "Group" + suffix,
},
requires: map[int][]string{
0: {"set", "s"},
1: {"group", "g"},
},
root: false,
})
}
}
func processLights() {
for lt, l := range ziggy.Lucifer.Lights {
suffix := ""
if l.Type != "" {
suffix = " (" + l.Type + ")"
}
suggestions[2] = append(suggestions[2],
completion{
Suggest: cli.Suggest{
Text: lt,
Description: "Light" + suffix,
},
requires: map[int][]string{
0: {"set", "s"},
1: {"light", "l"},
},
root: false,
})
}
}
func processBridges() {
for brd, b := range ziggy.Lucifer.Bridges {
suggestions[1] = append(suggestions[1],
completion{
Suggest: cli.Suggest{
Text: brd,
Description: "Bridge: " + b.Host,
},
requires: map[int][]string{0: {"use", "u"}},
root: false,
})
}
}
func completer(in cli.Document) []cli.Suggest {
c := in.CurrentLine()
if c == "" {
return []cli.Suggest{}
infields := strings.Fields(c)
var head = len(infields) - 1
if len(infields) == 0 {
head = 0
}
// args := strings.Fields(c)
var set []cli.Suggest
for command, subcommand := range suggestions {
head := in.CursorPositionCol()
if head > len(command.Text) {
head = len(command.Text)
}
tmpl := &cli.Document{Text: command.Text}
one := tmpl.GetWordAfterCursor()
if one != "" && one != command.Text {
set = append(set, cli.Suggest{Text: tmpl.Text})
}
if one == "use" && len(subcommand) > 1 {
set = append(set, cli.Suggest{Text: tmpl.Text})
}
for _, a := range subcommand {
if head > len(tmpl.Text+a.Text)+1 {
continue
}
tmpl = &cli.Document{Text: tmpl.Text + " " + a.Text}
two := tmpl.GetWordAfterCursorWithSpace()
if two != "" {
set = append(set, cli.Suggest{Text: tmpl.Text})
}
var sugs []cli.Suggest
for _, sug := range suggestions[head] {
if sug.qualifies(c) && strings.HasPrefix(sug.Text, in.GetWordBeforeCursor()) {
sugs = append(sugs, sug.Suggest)
}
}
return cli.FilterHasPrefix(set, c, false)
return sugs
}

@ -14,7 +14,7 @@ func cpuLoad(ctx context.Context) (chan int, error) {
loadChan := make(chan int, 10)
go func() {
for {
time.Sleep(250 * time.Millisecond)
time.Sleep(2 * time.Second)
cpu, err := syStats.GetCPU()
if err != nil {
return

@ -28,6 +28,7 @@ type Meta struct {
Bridges map[string]*Bridge
Lights map[string]*HueLight
Switches map[string]*huego.Sensor
Groups map[string]*huego.Group
*sync.RWMutex
}
@ -255,7 +256,11 @@ func (c *Bridge) getLights() error {
newlight.Log().Trace().Msg("+")
c.HueLights = append(c.HueLights, newlight)
Lucifer.Lock()
Lucifer.Lights[light.UniqueID] = newlight
name := strings.ReplaceAll(newlight.Name, " ", "_")
if _, ok := Lucifer.Lights[name]; ok {
name = fmt.Sprintf("%s_%d", name, 1)
}
Lucifer.Lights[name] = newlight
Lucifer.Unlock()
}
return nil