Compare commits

..

No commits in common. "74d9ac7346f98955e32d7a7b1220193a14fb2b7c" and "1ff60a88fad8b5606ebe0a1f6db90b4e012525d6" have entirely different histories.

6 changed files with 109 additions and 126 deletions

@ -32,7 +32,5 @@ With that being said, it should work fairly well to provide a command line inter
- **port scan to find offline (no call home) bridges on LAN** - **port scan to find offline (no call home) bridges on LAN**
- see gif above for demonstration - see gif above for demonstration
- config will automatically save when a bridge connection is established - config will automatically save when a bridge connection is established
- **trigger firmware updates for bridge and lights manually**
- e.g: `upgrade`
--- ---

@ -31,10 +31,9 @@ type ziggsCommand struct {
description string description string
aliases []string aliases []string
isAlias bool isAlias bool
requires int // number of arguments required
} }
func newZiggsCommand(react reactor, desc string, requires int, aliases ...string) *ziggsCommand { func newZiggsCommand(react reactor, desc string, aliases ...string) *ziggsCommand {
ret := &ziggsCommand{ ret := &ziggsCommand{
reactor: react, reactor: react,
aliases: aliases, aliases: aliases,

@ -15,18 +15,22 @@ const (
type completion struct { type completion struct {
cli.Suggest cli.Suggest
inner *ziggsCommand inner *ziggsCommand
requires map[int]map[string]bool requires map[int][]string
isAlias bool isAlias bool
root bool root bool
} }
func (c completion) qualifies(line string) bool { func (c completion) qualifies(line string) bool {
args := strings.Fields(line) args := strings.Fields(line)
if len(line) == 0 {
if len(args) <= 1 && c.root { return false
}
if c.root && len(args) < 1 {
return true return true
} }
/*if c.root && len(args) > 0 {
return false
}*/
if len(args) < len(c.requires) { if len(args) < len(c.requires) {
if extraDebug { if extraDebug {
log.Trace().Int("len(args)", len(args)).Int("len(c.requires)", len(c.requires)). log.Trace().Int("len(args)", len(args)).Int("len(c.requires)", len(c.requires)).
@ -41,60 +45,54 @@ func (c completion) qualifies(line string) bool {
} }
return false return false
} }
has := func(b []string, a string) bool {
for _, r := range b {
if a == r {
return true
}
}
return false
}
var count = 0 var count = 0
for i, a := range args { for i, a := range args {
i++ if has(c.requires[i], a) {
if _, ok := c.requires[i][a]; ok {
if extraDebug { if extraDebug {
log.Trace().Msgf("%v%s: found %s%v", grn, c.Text, a, rst) log.Trace().Msgf("%v%s: found %s%v", grn, c.Text, a, rst)
} }
count++ count++
} }
} }
if extraDebug && !(count >= len(c.requires)) {
log.Trace().Msgf("%v%s: count(%d) < len(c.requires)(%d)", red, c.Text, count, len(c.requires))
}
return count >= len(c.requires) return count >= len(c.requires)
} }
var suggestions map[int]map[string]*completion var suggestions map[int][]*completion
func init() { func init() {
Commands["ls"] = newZiggsCommand(cmdList, "list all lights, groups, scenes, rules, and schedules", 0) Commands["ls"] = newZiggsCommand(cmdList, "list all lights, groups, scenes, rules, and schedules")
Commands["schedules"] = newZiggsCommand(cmdSchedules, "list schedules", 0, Commands["schedules"] = newZiggsCommand(cmdSchedules, "list schedules", "lssched", "crontab")
"lssched", "crontab") Commands["rules"] = newZiggsCommand(cmdRules, "list rules", "lsrule")
Commands["rules"] = newZiggsCommand(cmdRules, "list rules", 0, "lsrule") Commands["sensors"] = newZiggsCommand(cmdSensors, "list sensors", "lssens")
Commands["sensors"] = newZiggsCommand(cmdSensors, "list sensors", 0, "lssens") Commands["scenes"] = newZiggsCommand(cmdScenes, "list scenes", "lsscene")
Commands["scenes"] = newZiggsCommand(cmdScenes, "list scenes", 0, "lsscene") Commands["lights"] = newZiggsCommand(cmdLights, "list lights", "lslight")
Commands["lights"] = newZiggsCommand(cmdLights, "list lights", 0, "lslight") Commands["groups"] = newZiggsCommand(cmdGroups, "list groups", "lsgrp")
Commands["groups"] = newZiggsCommand(cmdGroups, "list groups", 0, "lsgrp") Commands["create"] = newZiggsCommand(cmdCreate, "create a new object in bridge", "new", "mk")
Commands["create"] = newZiggsCommand(cmdCreate, "create a new object in bridge", 3, Commands["delete"] = newZiggsCommand(cmdDelete, "delete objects from bridges", "del", "remove")
"new", "mk") Commands["scan"] = newZiggsCommand(cmdScan, "scan for bridges/lights/sensors", "search", "find")
Commands["delete"] = newZiggsCommand(cmdDelete, "delete objects from bridges", 2, Commands["rename"] = newZiggsCommand(cmdRename, "rename object in bridge", "mv")
"del", "remove", "rm") Commands["adopt"] = newZiggsCommand(cmdAdopt, "adopt new lights to the bridge")
Commands["scan"] = newZiggsCommand(cmdScan, "scan for bridges/lights/sensors", 0, Commands["dump"] = newZiggsCommand(cmdDump, "dump target object JSON to a file")
"search", "find") Commands["load"] = newZiggsCommand(cmdLoad, "load JSON from a file into the bridge")
Commands["rename"] = newZiggsCommand(cmdRename, "rename object in bridge", 3, "mv") Commands["set"] = newZiggsCommand(cmdSet, "update object properties in bridge")
Commands["adopt"] = newZiggsCommand(cmdAdopt, "adopt new lights to the bridge", 0) Commands["fwupdate"] = newZiggsCommand(cmdFirmwareUpdate, "inform bridge to check for updates",
Commands["dump"] = newZiggsCommand(cmdDump, "dump target object JSON to a file", 1) "fwup", "upgrade")
Commands["load"] = newZiggsCommand(cmdLoad, "load JSON from a file into the bridge", 1) Commands["info"] = newZiggsCommand(cmdInfo, "show information about a bridge", "uname")
Commands["set"] = newZiggsCommand(cmdSet, "update object properties in bridge", 3)
Commands["upgrade"] = newZiggsCommand(cmdFirmwareUpdate, "inform bridge to check for updates", 0,
"fwup", "upgrade", "fwupdate")
Commands["info"] = newZiggsCommand(cmdInfo, "show information about a bridge", 0, "uname")
initCompletion() initCompletion()
} }
func initCompletion() { func initCompletion() {
suggestions = make(map[int]map[string]*completion) suggestions = make(map[int][]*completion)
suggestions[0] = make(map[string]*completion) suggestions[0] = []*completion{
suggestions[1] = make(map[string]*completion) {Suggest: cli.Suggest{Text: "lights"}, inner: Commands["lights"]},
suggestions[2] = make(map[string]*completion)
/* {Suggest: cli.Suggest{Text: "lights"}, inner: Commands["lights"]},
{Suggest: cli.Suggest{Text: "groups"}, inner: Commands["groups"]}, {Suggest: cli.Suggest{Text: "groups"}, inner: Commands["groups"]},
{Suggest: cli.Suggest{Text: "rules"}, inner: Commands["rules"]}, {Suggest: cli.Suggest{Text: "rules"}, inner: Commands["rules"]},
{Suggest: cli.Suggest{Text: "scenes"}, inner: Commands["scenes"]}, {Suggest: cli.Suggest{Text: "scenes"}, inner: Commands["scenes"]},
@ -109,72 +107,63 @@ func initCompletion() {
{Suggest: cli.Suggest{Text: "dump"}, inner: Commands["dump"]}, {Suggest: cli.Suggest{Text: "dump"}, inner: Commands["dump"]},
{Suggest: cli.Suggest{Text: "load"}, inner: Commands["load"]}, {Suggest: cli.Suggest{Text: "load"}, inner: Commands["load"]},
{Suggest: cli.Suggest{Text: "use", Description: "select bridge to perform actions on"}}, {Suggest: cli.Suggest{Text: "use", Description: "select bridge to perform actions on"}},
{Suggest: cli.Suggest{Text: "clear", Description: "clear screen"}},
{Suggest: cli.Suggest{Text: "exit", Description: "exit ziggs"}}, {Suggest: cli.Suggest{Text: "exit", Description: "exit ziggs"}},
*/
suggestions[0]["clear"] = &completion{Suggest: cli.Suggest{Text: "clear", Description: "clear screen"}}
suggestions[0]["exit"] = &completion{Suggest: cli.Suggest{Text: "exit", Description: "exit ziggs"}}
suggestions[0]["help"] = &completion{Suggest: cli.Suggest{Text: "help", Description: "show help"}}
for name, cmd := range Commands {
suggestions[0][name] = &completion{Suggest: cli.Suggest{Text: name}, inner: cmd, root: cmd.requires == 0}
if cmd.description != "" {
suggestions[0][name].Description = cmd.description
}
suggestions[0][name].requires = map[int]map[string]bool{}
} }
suggestions[1] = map[string]*completion{ for _, sug := range suggestions[0] {
"group": {Suggest: cli.Suggest{Text: "group", Description: "target group"}}, sug.requires = map[int][]string{}
"light": {Suggest: cli.Suggest{Text: "light", Description: "target light"}}, sug.root = true
"scene": {Suggest: cli.Suggest{Text: "scene", Description: "target scene"}}, if sug.inner != nil {
"schedule": {Suggest: cli.Suggest{Text: "schedule", Description: "target schedule"}}, sug.Suggest.Description = sug.inner.description
"sensor": {Suggest: cli.Suggest{Text: "sensor", Description: "target sensor"}}, }
"config": {Suggest: cli.Suggest{Text: "config", Description: "target bridge config"}}, if sug.inner != nil && len(sug.inner.aliases) > 0 {
for _, a := range sug.inner.aliases {
suggestions[0] = append(suggestions[0], &completion{
Suggest: cli.Suggest{Text: a, Description: sug.Description},
inner: sug.inner,
root: true,
isAlias: true,
})
}
}
}
suggestions[1] = []*completion{
{Suggest: cli.Suggest{Text: "group", Description: "target group"}},
{Suggest: cli.Suggest{Text: "light", Description: "target light"}},
{Suggest: cli.Suggest{Text: "scene", Description: "target scene"}},
{Suggest: cli.Suggest{Text: "schedule", Description: "target schedule"}},
{Suggest: cli.Suggest{Text: "sensor", Description: "target sensor"}},
{Suggest: cli.Suggest{Text: "config", Description: "target bridge config"}},
} }
for _, sug := range suggestions[1] { for _, sug := range suggestions[1] {
sug.requires = map[int]map[string]bool{1: { sug.requires = map[int][]string{0: {"delete", "del", "set", "s", "rename", "mv", "dump", "load"}}
"delete": true, "del": true, "set": true, "s": true, "rename": true, "mv": true, "dump": true, "load": true},
}
sug.root = false sug.root = false
} }
delCompletion := []*completion{ delCompletion := []*completion{
{Suggest: cli.Suggest{Text: "scene", Description: "target scene"}}, {Suggest: cli.Suggest{Text: "scene", Description: "target scene"}},
{Suggest: cli.Suggest{Text: "schedule", Description: "target schedule"}}, {Suggest: cli.Suggest{Text: "schedule", Description: "target schedule"}},
{Suggest: cli.Suggest{Text: "sensor", Description: "target sensor"}}, {Suggest: cli.Suggest{Text: "sensor", Description: "target sensor"}},
{Suggest: cli.Suggest{Text: "group", Description: "target group"}},
} }
for _, sug := range delCompletion { for _, sug := range delCompletion {
sug.requires = map[int]map[string]bool{1: {"delete": true, "del": true}} sug.requires = map[int][]string{0: {"delete", "del"}}
sug.root = false sug.root = false
} }
suggestions[1] = append(suggestions[1], delCompletion...)
} }
func completer(in cli.Document) []cli.Suggest { func completer(in cli.Document) []cli.Suggest {
c := in.CurrentLine() c := in.CurrentLine()
infields := strings.Fields(c) infields := strings.Fields(c)
var head = len(infields) - 1 var head = len(infields) - 1
if head < 0 { if len(infields) == 0 {
head = 0 head = 0
} }
if head == 1 {
head = 1
}
if head > 0 && in.LastKeyStroke() == ' ' {
head++
}
if extraDebug {
log.Trace().Int("head", head).Msgf("completing %v", infields)
}
var sugs []cli.Suggest var sugs []cli.Suggest
for _, sug := range suggestions[head] { for _, sug := range suggestions[head] {
if sug.qualifies(c) { if sug.qualifies(c) && strings.HasPrefix(sug.Text, in.GetWordBeforeCursor()) {
if in.GetWordBeforeCursor() != "" && strings.HasPrefix(sug.Text, in.GetWordBeforeCursor()) { sugs = append(sugs, sug.Suggest)
sugs = append(sugs, sug.Suggest)
}
} }
} }
return sugs return sugs

@ -13,18 +13,18 @@ func processGroups(grps map[string]*huego.Group) {
if g.Type != "" { if g.Type != "" {
suffix = " (" + g.Type + ")" suffix = " (" + g.Type + ")"
} }
suggestions[2] = append(suggestions[2],
suggestions[2][grp] = &completion{ &completion{
Suggest: cli.Suggest{ Suggest: cli.Suggest{
Text: grp, Text: grp,
Description: "Group" + suffix, Description: "Group" + suffix,
}, },
requires: map[int]map[string]bool{ requires: map[int][]string{
1: {"set": true, "s": true, "delete": true, "d": true}, 0: {"set", "s", "delete", "d"},
2: {"group": true, "g": true}, 1: {"group", "g"},
}, },
root: false, root: false,
} })
} }
} }
@ -34,29 +34,31 @@ func processLights() {
if l.Type != "" { if l.Type != "" {
suffix = " (" + l.Type + ")" suffix = " (" + l.Type + ")"
} }
suggestions[2][lt] = &completion{ suggestions[2] = append(suggestions[2],
Suggest: cli.Suggest{ &completion{
Text: lt, Suggest: cli.Suggest{
Description: "Light" + suffix, Text: lt,
}, Description: "Light" + suffix,
requires: map[int]map[string]bool{ },
1: {"set": true, "s": true, "delete": true, "d": true}, requires: map[int][]string{
2: {"light": true, "l": true}, 0: {"set", "s", "delete", "d", "rename", "r"},
}, 1: {"light", "l"},
root: false, },
} root: false,
})
} }
} }
func processBridges() { func processBridges() {
for brd, b := range ziggy.Lucifer.Bridges { for brd, b := range ziggy.Lucifer.Bridges {
suggestions[1]["bridge"] = &completion{ suggestions[1] = append(suggestions[1],
Suggest: cli.Suggest{ &completion{
Text: brd, Suggest: cli.Suggest{
Description: "Bridge: " + b.Host, Text: brd,
}, Description: "Bridge: " + b.Host,
requires: map[int]map[string]bool{0: {"use": true, "u": true}}, },
root: false, requires: map[int][]string{0: {"use", "u"}},
} root: false,
})
} }
} }

@ -2,7 +2,6 @@ package cli
import ( import (
"context" "context"
"strings"
"time" "time"
"github.com/amimof/huego" "github.com/amimof/huego"
@ -27,11 +26,7 @@ func cmdFirmwareUpdate(br *ziggy.Bridge, args []string) error {
if resp, err = br.UpdateConfig(&huego.Config{SwUpdate2: huego.SwUpdate2{CheckForUpdate: true}}); err == nil { if resp, err = br.UpdateConfig(&huego.Config{SwUpdate2: huego.SwUpdate2{CheckForUpdate: true}}); err == nil {
log.Info().Msgf("response: %v", resp) log.Info().Msgf("response: %v", resp)
} else { } else {
if strings.Contains(err.Error(), "devicetype") { log.Warn().Msgf("failed to issue update command: %v", err)
log.Debug().Msgf("non-consequential error response: %v", err)
} else {
log.Warn().Msgf("failed to issue update command: %v", err)
}
} }
ctx, cancel := watchUpdateStatus(br, 5*time.Minute) ctx, cancel := watchUpdateStatus(br, 5*time.Minute)
@ -70,7 +65,7 @@ func watchUpdateStatus(br *ziggy.Bridge, timeout time.Duration) (context.Context
case "noupdates": case "noupdates":
log.Info().Msg("no updates available") log.Info().Msg("no updates available")
cancel() cancel()
case "allreadytoinstall", "anyreadytoinstall": case "allreadytoinstall":
log.Info().Msg("all updates ready to install") log.Info().Msg("all updates ready to install")
log.Info().Msg("installing updates...") log.Info().Msg("installing updates...")
if _, err := br.UpdateConfig(&huego.Config{SwUpdate2: huego.SwUpdate2{Install: true}}); err != nil { if _, err := br.UpdateConfig(&huego.Config{SwUpdate2: huego.SwUpdate2{Install: true}}); err != nil {
@ -124,7 +119,7 @@ func streamUpdateStatus(br *ziggy.Bridge, ctx context.Context, cancel context.Ca
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
continue continue
} }
log.Debug().Msgf("bridge update state: %s", c.SwUpdate2.State) log.Trace().Msgf("bridge update state: %s", c.SwUpdate2.State)
ch <- c.SwUpdate2.State ch <- c.SwUpdate2.State
} }
} }

@ -35,7 +35,7 @@ type Config struct {
// SwUpdate contains information related to software updates. Deprecated in 1.20 // SwUpdate contains information related to software updates. Deprecated in 1.20
type SwUpdate struct { type SwUpdate struct {
CheckForUpdate bool `json:"checkforupdate,omitempty"` CheckForUpdate bool `json:"checkforupdate,omitempty"`
DeviceTypes DeviceTypes `json:"devicetypes,omitempty"` DeviceTypes DeviceTypes `json:"devicetypes"`
UpdateState uint8 `json:"updatestate,omitempty"` UpdateState uint8 `json:"updatestate,omitempty"`
Notify bool `json:"notify,omitempty"` Notify bool `json:"notify,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`