Skip to content

Commit

Permalink
New setup command (#34)
Browse files Browse the repository at this point in the history
The setup command automatically discovers the Hue Bridge and ask user to
push the pairing button.


![openhue_setup](https://github.com/openhue/openhue-cli/assets/39826516/e2a6d1de-6296-437b-a85b-5785f4583652)
  • Loading branch information
thibauult authored Jan 13, 2024
1 parent 1944b0c commit 235a655
Show file tree
Hide file tree
Showing 10 changed files with 195 additions and 126 deletions.
2 changes: 1 addition & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func Execute(buildInfo *openhue.BuildInfo) {

// add sub commands
root.AddCommand(version.NewCmdVersion(ctx))
root.AddCommand(setup.NewCmdAuth(ctx.Io))
root.AddCommand(setup.NewCmdSetup(ctx.Io))
root.AddCommand(setup.NewCmdDiscover(ctx.Io))
root.AddCommand(setup.NewCmdConfigure())

Expand Down
69 changes: 0 additions & 69 deletions cmd/setup/auth.go

This file was deleted.

6 changes: 3 additions & 3 deletions cmd/setup/configure.go → cmd/setup/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
)

const (
docShortConfigure = `Configure your local Philips Hue environment`
docShortConfigure = `Manual openhue CLI setup`
docLongConfigure = `
The setup command must be run as a prerequisite for all resource related commands (controlling lights, rooms, scenes, etc.)
Expand All @@ -24,7 +24,7 @@ func NewCmdConfigure() *cobra.Command {
o := Options{}

cmd := &cobra.Command{
Use: "configure",
Use: "config",
GroupID: "config",
Short: docShortConfigure,
Long: docLongConfigure,
Expand All @@ -35,7 +35,7 @@ func NewCmdConfigure() *cobra.Command {
Key: o.key,
}

err := c.Save()
_, err := c.Save()
cobra.CheckErr(err)
},
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/setup/configure_test.go → cmd/setup/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ func TestNewCmdConfigure(t *testing.T) {

cmd := NewCmdConfigure()

assert.ThatCmdUseIs(t, cmd, "configure")
assert.ThatCmdUseIs(t, cmd, "config")
assert.ThatCmdGroupIs(t, cmd, "config")

}
32 changes: 7 additions & 25 deletions cmd/setup/discover.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,13 @@ package setup

import (
"fmt"
"github.com/brutella/dnssd"
"github.com/spf13/cobra"
"golang.org/x/net/context"
"net"
"openhue-cli/openhue"
"os"
"strings"
"openhue-cli/util/mdns"
"time"
)

const serviceName = "_hue._tcp"
const domain = ".local"

// NewCmdDiscover represents the discover command
func NewCmdDiscover(io openhue.IOStreams) *cobra.Command {

Expand All @@ -22,26 +18,12 @@ func NewCmdDiscover(io openhue.IOStreams) *cobra.Command {
Short: "Hue Bridge discovery",
Long: `Discover your Hue Bridge on your local network using the mDNS Service Discovery`,
Run: func(cmd *cobra.Command, args []string) {
DiscoverBridge(io)

ip := make(chan *net.IP)
go mdns.DiscoverBridge(ip, 5*time.Second)
fmt.Fprintf(io.Out, "%s\n", <-ip)
},
}

return cmd
}

func DiscoverBridge(io openhue.IOStreams) {
service := fmt.Sprintf("%s.%s.", strings.Trim(serviceName, "."), strings.Trim(domain, "."))

foundFn := func(e dnssd.BrowseEntry) {

for _, ip := range e.IPs {
if ip.To4() != nil { // we want to display IPv4 address only
fmt.Fprintf(io.Out, "\nFound '%s' with IP '%s'\n", strings.Replace(e.Name, "\\", "", 3), ip)
os.Exit(0)
}
}
}

err := dnssd.LookupType(context.Background(), service, foundFn, nil)
cobra.CheckErr(err)
}
113 changes: 113 additions & 0 deletions cmd/setup/setup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package setup

import (
"context"
"errors"
"fmt"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"net"
"openhue-cli/openhue"
"openhue-cli/openhue/gen"
"openhue-cli/util/mdns"
"os"
"time"
)

type CmdSetupOptions struct {
deviceType string
generateClientKey bool
}

func NewCmdSetup(io openhue.IOStreams) *cobra.Command {

o := CmdSetupOptions{}

cmd := &cobra.Command{
Use: "setup",
GroupID: "config",
Short: "Automatic openhue CLI setup",
Long: `
The setup command will automatically discover the Hue Bridge connected to your local network and ask you to push
the bridge button to perform initial pairing.
`,
Run: func(cmd *cobra.Command, args []string) {
startSetup(io, &o)
},
}

cmd.Flags().StringVarP(&o.deviceType, "devicetype", "d", getHostName(), "Device identifier")
cmd.Flags().BoolVarP(&o.generateClientKey, "generateclientkey", "k", true, "Generate the client key")

return cmd
}

func startSetup(io openhue.IOStreams, o *CmdSetupOptions) {
ipChan := make(chan *net.IP)
go mdns.DiscoverBridge(ipChan, 5*time.Second)
ip := <-ipChan

if ip == nil {
fmt.Fprintf(io.ErrOut, "❌ Unable to discover your Hue Bridge on your local network\n")
return
}

fmt.Fprintf(io.Out, "[OK] Found Hue Bridge with IP '%s'\n", ip)

client := openhue.NewOpenHueClientNoAuth(ip.String())

fmt.Fprintln(io.Out, "[..] Please push the button on your Hue Bridge")
done := false
for done == false {
fmt.Fprintf(io.Out, ".")
key, err := tryAuth(client, o.toAuthenticateBody())
if err != nil {
time.Sleep(1 * time.Second)
continue
}
done = true
fmt.Fprintf(io.Out, "\n")
log.Info("Hue Application Key is ", key)
fmt.Fprintln(io.Out, "[OK] Successfully paired openhue with your Hue Bridge!")
path, err := saveConfig(ip.String(), key)
if err != nil {
fmt.Fprintf(io.ErrOut, "[KO] Unable to save config")
}
fmt.Fprintln(io.Out, "[OK] Configuration saved in file", path)
}
}

func (o *CmdSetupOptions) toAuthenticateBody() gen.AuthenticateJSONRequestBody {
body := gen.AuthenticateJSONRequestBody{}
body.Devicetype = &o.deviceType
body.Generateclientkey = &o.generateClientKey
return body
}

func tryAuth(client *gen.ClientWithResponses, body gen.AuthenticateJSONRequestBody) (string, error) {

resp, err := client.AuthenticateWithResponse(context.Background(), body)
cobra.CheckErr(err)

auth := (*resp.JSON200)[0]
if auth.Error != nil {
return "", errors.New(*auth.Error.Description)
}

return *auth.Success.Username, nil
}

func saveConfig(bridge string, key string) (string, error) {
c := openhue.Config{
Bridge: bridge,
Key: key,
}

return c.Save()
}

func getHostName() string {
hostname, err := os.Hostname()
cobra.CheckErr(err)
return hostname
}
7 changes: 4 additions & 3 deletions cmd/setup/auth_test.go → cmd/setup/setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import (
"testing"
)

func TestNewCmdAuth(t *testing.T) {
func TestNewCmdSetup(t *testing.T) {

cmd := NewCmdAuth(openhue.NewTestIOStreamsDiscard())
cmd := NewCmdSetup(openhue.NewTestIOStreamsDiscard())

assert.ThatCmdUseIs(t, cmd, "auth")
assert.ThatCmdUseIs(t, cmd, "setup")
assert.ThatCmdGroupIs(t, cmd, "config")

}
44 changes: 23 additions & 21 deletions openhue/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,17 @@ import (
)

// CommandsWithNoConfig contains the list of commands that don't require the configuration to exist
var CommandsWithNoConfig = []string{"configure", "help", "discover", "auth", "version", "completion"}
var CommandsWithNoConfig = []string{"setup", "config", "help", "discover", "auth", "version", "completion"}
var configPath string

func init() {
// Find home directory.
home, err := os.UserHomeDir()
cobra.CheckErr(err)

configPath = filepath.Join(home, "/.openhue")
_ = os.MkdirAll(configPath, os.ModePerm)
}

type Config struct {
// The IP of the Philips HUE Bridge
Expand All @@ -25,52 +35,44 @@ type Config struct {
Key string
}

func (c *Config) Load() {

// Find home directory.
home, err := os.UserHomeDir()
cobra.CheckErr(err)

var configPath = filepath.Join(home, "/.openhue")
_ = os.MkdirAll(configPath, os.ModePerm)

logger.Init(configPath)
func (c *Config) GetConfigFile() string {
return filepath.Join(configPath, "config.yaml")
}

// Search config in home directory with name ".openhue" (without an extension).
viper.AddConfigPath(configPath)
viper.SetConfigName("config")
viper.SetConfigType("yaml")
func (c *Config) Load() {
logger.Init(filepath.Join(configPath, "openhue.log"))
viper.SetConfigFile(c.GetConfigFile())

// When trying to run CLI without configuration
configDoesNotExist := viper.ReadInConfig() != nil
if configDoesNotExist && len(os.Args) > 1 && !slices.Contains(CommandsWithNoConfig, os.Args[1]) {
fmt.Println("\nopenhue-cli not configured yet, please run the 'configure' command")
fmt.Println("\nopenhue-cli not configured yet, please run the 'setup' command")
os.Exit(0)
}

c.Bridge = viper.GetString("Bridge")
c.Key = viper.GetString("Key")
}

func (c *Config) Save() error {
func (c *Config) Save() (string, error) {

if len(c.Bridge) == 0 {
return errors.New("'bridge' value not set in config")
return "", errors.New("'bridge' value not set in config")
}

if len(c.Key) == 0 {
return errors.New("'key' value not set in config")
return "", errors.New("'key' value not set in config")
}

viper.Set("Bridge", c.Bridge)
viper.Set("Key", c.Key)

err := viper.SafeWriteConfig()
if err != nil {
return viper.WriteConfig()
return c.GetConfigFile(), viper.WriteConfig()
}

return nil
return c.GetConfigFile(), nil
}

// NewOpenHueClient Creates a new NewClientWithResponses for a given server and hueApplicationKey.
Expand Down
5 changes: 2 additions & 3 deletions util/logger/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@ package logger
import (
log "github.com/sirupsen/logrus"
"os"
"path/filepath"
)

func Init(configPath string) {
func Init(path string) {

if isProd() {
// If the file doesn't exist, create it or append to the file
file, err := os.OpenFile(filepath.Join(configPath, "openhue.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
log.Fatal(err)
}
Expand Down
Loading

0 comments on commit 235a655

Please sign in to comment.