go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/experiments/huectl/pkg/command/helpers.go (about)

     1  /*
     2  
     3  Copyright (c) 2023 - Present. Will Charczuk. All rights reserved.
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository.
     5  
     6  */
     7  
     8  package command
     9  
    10  import (
    11  	"context"
    12  	"encoding/json"
    13  	"fmt"
    14  	"io"
    15  	"os"
    16  	"reflect"
    17  	"strings"
    18  	"text/template"
    19  
    20  	"github.com/urfave/cli/v2"
    21  	"go.charczuk.com/sdk/ansi"
    22  
    23  	"go.charczuk.com/experiments/huectl/pkg/hue"
    24  )
    25  
    26  const (
    27  	flagAddr       = "addr"
    28  	flagConfig     = "config"
    29  	flagDeviceType = "device-type"
    30  	flagGroupID    = "group-id"
    31  	flagGroupName  = "group-name"
    32  	flagLightID    = "light-id"
    33  	flagLightName  = "light-name"
    34  	flagOutput     = "output"
    35  	flagQuiet      = "quiet"
    36  	flagTemplate   = "template"
    37  	flagUsername   = "username"
    38  	flagVerbose    = "verbose"
    39  )
    40  
    41  // DefaultFlags are the default (or persistent) flags.
    42  var DefaultFlags = []cli.Flag{
    43  	&cli.StringFlag{
    44  		Name:    flagConfig,
    45  		Usage:   "The json config file path",
    46  		Value:   os.ExpandEnv("${HOME}/.config/huectl/config.json"),
    47  		EnvVars: []string{"HUE_CONFIG"},
    48  	},
    49  	&cli.StringFlag{
    50  		Name:    flagUsername,
    51  		Usage:   "The hue username to authenticate with",
    52  		EnvVars: []string{"HUE_USERNAME"},
    53  	},
    54  	&cli.StringFlag{
    55  		Name:    flagAddr,
    56  		Usage:   "The hue bridge local address",
    57  		EnvVars: []string{"HUE_ADDR"},
    58  	},
    59  	&cli.BoolFlag{
    60  		Name:    flagQuiet,
    61  		Aliases: []string{"q"},
    62  		Usage:   "If command output should be suppressed",
    63  	},
    64  	&cli.BoolFlag{
    65  		Name:    flagVerbose,
    66  		Aliases: []string{"v"},
    67  		Usage:   "If verbose command should be shown",
    68  	},
    69  }
    70  
    71  // OutputFlags are common output related flags.
    72  var OutputFlags = []cli.Flag{
    73  	&cli.StringFlag{
    74  		Name:    flagOutput,
    75  		Aliases: []string{"o"},
    76  		Usage:   "The output format (json|table|template)",
    77  		EnvVars: []string{"HUE_OUTPUT"},
    78  	},
    79  	&cli.StringFlag{
    80  		Name:    flagTemplate,
    81  		Aliases: []string{"t"},
    82  		Usage:   "The output template",
    83  		EnvVars: []string{"HUE_OUTPUT_TEMPLATE"},
    84  	},
    85  }
    86  
    87  // GroupFlags are group related flags.
    88  var GroupFlags = []cli.Flag{
    89  	&cli.IntFlag{
    90  		Name:    flagGroupID,
    91  		Aliases: []string{"id"},
    92  		Usage:   "The group `ID` field of the group in question, exclusive with --group-name",
    93  	},
    94  	&cli.StringFlag{
    95  		Name:    flagGroupName,
    96  		Aliases: []string{"n", "name"},
    97  		Usage:   "The group `Name` field of the group in question, exclusive with --group-id",
    98  	},
    99  }
   100  
   101  // LightFlags are light related flags.
   102  var LightFlags = []cli.Flag{
   103  	&cli.IntFlag{
   104  		Name:    flagLightID,
   105  		Aliases: []string{"id"},
   106  		Usage:   "The `ID` field of the light in question",
   107  	},
   108  	&cli.StringFlag{
   109  		Name:    flagLightName,
   110  		Aliases: []string{"name", "n"},
   111  		Usage:   "The `Name` field of the light in question",
   112  	},
   113  }
   114  
   115  func groupHelper(c *cli.Context) (*hue.Group, *hue.Bridge, error) {
   116  	bridge, err := initHelper(c)
   117  	if err != nil {
   118  		return nil, nil, err
   119  	}
   120  
   121  	if c.Int(flagGroupID) > 0 && c.String(flagGroupName) != "" {
   122  		return nil, nil, fmt.Errorf("please specify one of --group-id or --group-name")
   123  	}
   124  	var group *hue.Group
   125  	if c.Int(flagGroupID) > 0 || c.String(flagGroupName) != "" {
   126  		group, err = getGroup(c, &bridge, getGroupArgs{
   127  			ID:   c.Int(flagGroupID),
   128  			Name: c.String(flagGroupName),
   129  		})
   130  		if err != nil {
   131  			return nil, nil, err
   132  		}
   133  	}
   134  	return group, &bridge, nil
   135  }
   136  
   137  func lightHelper(c *cli.Context) (*hue.Light, *hue.Bridge, error) {
   138  	bridge, err := initHelper(c)
   139  	if err != nil {
   140  		return nil, nil, err
   141  	}
   142  
   143  	if c.Int(flagLightID) > 0 && c.String(flagLightName) != "" {
   144  		return nil, nil, fmt.Errorf("please specify one of --light-id or --light-name")
   145  	}
   146  	var light *hue.Light
   147  	if c.Int(flagLightID) > 0 || c.String(flagLightName) != "" {
   148  		light, err = getLight(c, &bridge, getLightArgs{
   149  			ID:   c.Int(flagLightID),
   150  			Name: c.String(flagLightName),
   151  		})
   152  		if err != nil {
   153  			return nil, nil, err
   154  		}
   155  	}
   156  	return light, &bridge, nil
   157  }
   158  
   159  func initHelper(c *cli.Context) (hue.Bridge, error) {
   160  	c.Context = hue.WithVerbose(c.Context, c.Bool(flagVerbose))
   161  	return getBridge(c)
   162  }
   163  
   164  type configKey struct{}
   165  
   166  func withConfig(ctx context.Context, cfg config) context.Context {
   167  	return context.WithValue(ctx, configKey{}, cfg)
   168  }
   169  
   170  func getConfigContext(ctx context.Context) (cfg config, ok bool) {
   171  	if value := ctx.Value(configKey{}); value != nil {
   172  		cfg, ok = value.(config)
   173  		return
   174  	}
   175  	return
   176  }
   177  
   178  type bridgeKey struct{}
   179  
   180  func withBridge(ctx context.Context, b hue.Bridge) context.Context {
   181  	return context.WithValue(ctx, bridgeKey{}, b)
   182  }
   183  
   184  func getBridgeContext(ctx context.Context) (b hue.Bridge, ok bool) {
   185  	if value := ctx.Value(bridgeKey{}); value != nil {
   186  		b, ok = value.(hue.Bridge)
   187  		return
   188  	}
   189  	return
   190  }
   191  
   192  type config struct {
   193  	Username string `json:"username"`
   194  	Addr     string `json:"addr"`
   195  }
   196  
   197  func readJSON(obj interface{}, path string) error {
   198  	f, err := os.Open(path)
   199  	if err != nil {
   200  		return err
   201  	}
   202  	defer f.Close()
   203  	return json.NewDecoder(f).Decode(obj)
   204  }
   205  
   206  func readConfig(c *config, path string) error {
   207  	return readJSON(c, path)
   208  }
   209  
   210  func getConfig(c *cli.Context) (cfg config, ok bool, err error) {
   211  	if cfg, ok = getConfigContext(c.Context); ok {
   212  		return
   213  	}
   214  
   215  	if configPath := c.String(flagConfig); configPath != "" {
   216  		if _, statErr := os.Stat(configPath); statErr == nil {
   217  			if err = readConfig(&cfg, configPath); err != nil {
   218  				return
   219  			}
   220  			ok = true
   221  		}
   222  	}
   223  	return
   224  }
   225  
   226  func getBridge(c *cli.Context) (b hue.Bridge, err error) {
   227  	var ok bool
   228  	if b, ok = getBridgeContext(c.Context); ok {
   229  		return
   230  	}
   231  
   232  	var cfg config
   233  	cfg, ok, err = getConfig(c)
   234  	if err != nil {
   235  		return
   236  	}
   237  	if ok {
   238  		b.Username = cfg.Username
   239  		b.Addr = cfg.Addr
   240  	}
   241  
   242  	if b.Username == "" {
   243  		username := c.String(flagUsername)
   244  		if username == "" {
   245  			err = fmt.Errorf("--user or $HUE_USERNAME is required to be set")
   246  			return
   247  		}
   248  		b.Username = username
   249  	}
   250  
   251  	if b.Addr == "" {
   252  		hueAddr := c.String(flagAddr)
   253  		if hueAddr == "" {
   254  			found, ok, discoverErr := hue.DiscoverFirst(c.Context)
   255  			if discoverErr != nil {
   256  				err = discoverErr
   257  				return
   258  			}
   259  			if !ok {
   260  				err = fmt.Errorf("--addr unset and no bridges discovered")
   261  				return
   262  			}
   263  			b.Addr = found.Addr
   264  		} else {
   265  			b.Addr = hueAddr
   266  		}
   267  	}
   268  	c.Context = withBridge(c.Context, b)
   269  	return
   270  }
   271  
   272  func output(c *cli.Context, wr io.Writer, obj interface{}) error {
   273  	switch c.String(flagOutput) {
   274  	case "json":
   275  		return json.NewEncoder(wr).Encode(obj)
   276  	case "table":
   277  		return ansi.TableForSlice(wr, obj)
   278  	case "template":
   279  		tmpl, err := template.New("huectl").Parse(c.String(flagTemplate) + "\n")
   280  		if err != nil {
   281  			return err
   282  		}
   283  		return iterate(obj, func(v interface{}) error {
   284  			return tmpl.Execute(wr, v)
   285  		})
   286  	default:
   287  		return ansi.TableForSlice(wr, obj)
   288  	}
   289  }
   290  
   291  func iterate(collection interface{}, fn func(interface{}) error) error {
   292  	cv := reflect.ValueOf(collection)
   293  	for cv.Kind() == reflect.Ptr {
   294  		cv = cv.Elem()
   295  	}
   296  	if cv.Kind() != reflect.Slice {
   297  		return fmt.Errorf("cannot iterate over non-slice collection")
   298  	}
   299  
   300  	var err error
   301  	for row := 0; row < cv.Len(); row++ {
   302  		if err = fn(cv.Index(row)); err != nil {
   303  			return err
   304  		}
   305  	}
   306  	return nil
   307  }
   308  
   309  type getGroupArgs struct {
   310  	ID   int
   311  	Name string
   312  }
   313  
   314  func getGroup(c *cli.Context, bridge *hue.Bridge, args getGroupArgs) (*hue.Group, error) {
   315  	if args.Name != "" {
   316  		groups, err := bridge.GetGroups(c.Context)
   317  		if err != nil {
   318  			return nil, err
   319  		}
   320  		for _, group := range groups {
   321  			if strings.EqualFold(group.Name, args.Name) {
   322  				return &group, nil
   323  			}
   324  		}
   325  		return nil, fmt.Errorf("group with name %q not found", args.Name)
   326  	}
   327  
   328  	group, err := bridge.GetGroup(c.Context, args.ID)
   329  	if err != nil {
   330  		return nil, err
   331  	}
   332  	if group == nil {
   333  		return nil, fmt.Errorf("group with id %d not found", args.ID)
   334  	}
   335  	return group, nil
   336  }
   337  
   338  type getSceneArgs struct {
   339  	ID   string
   340  	Name string
   341  }
   342  
   343  func getScene(c *cli.Context, bridge *hue.Bridge, args getSceneArgs) (*hue.Scene, error) {
   344  	if args.Name != "" {
   345  		scenes, err := bridge.GetScenes(c.Context)
   346  		if err != nil {
   347  			return nil, err
   348  		}
   349  		for _, scene := range scenes {
   350  			if strings.EqualFold(scene.Name, args.Name) {
   351  				return &scene, nil
   352  			}
   353  		}
   354  		return nil, fmt.Errorf("scene with name %q not found", args.Name)
   355  	}
   356  
   357  	scene, err := bridge.GetScene(c.Context, args.ID)
   358  	if err != nil {
   359  		return nil, err
   360  	}
   361  	if scene == nil {
   362  		return nil, fmt.Errorf("scene with id %v not found", args.ID)
   363  	}
   364  	return scene, nil
   365  }
   366  
   367  type getLightArgs struct {
   368  	ID   int
   369  	Name string
   370  }
   371  
   372  func getLight(c *cli.Context, bridge *hue.Bridge, args getLightArgs) (*hue.Light, error) {
   373  	if args.Name != "" {
   374  		lights, err := bridge.GetLights(c.Context)
   375  		if err != nil {
   376  			return nil, err
   377  		}
   378  		for _, light := range lights {
   379  			if strings.EqualFold(light.Name, args.Name) {
   380  				return &light, nil
   381  			}
   382  		}
   383  		return nil, fmt.Errorf("light with name %q not found", args.Name)
   384  	}
   385  
   386  	light, err := bridge.GetLight(c.Context, args.ID)
   387  	if err != nil {
   388  		return nil, err
   389  	}
   390  	if light == nil {
   391  		return nil, fmt.Errorf("light with id %d not found", args.ID)
   392  	}
   393  	return light, nil
   394  }
   395  
   396  func printf(c *cli.Context, format string, args ...any) {
   397  	if !c.Bool(flagQuiet) {
   398  		fmt.Printf(format, args...)
   399  	}
   400  }