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 }