/*Package cmd - root command Copyright © 2020 Jeffrey C. Ollie This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package cmd import ( "fmt" "image/color" "log" "os" "sync" "git.ocjtech.us/jeff/mystreamdeck/internal" "github.com/go-fonts/dejavu/dejavusans" "github.com/golang/freetype" "github.com/golang/freetype/truetype" "github.com/jcollie/go-homeassistant" "github.com/jcollie/go-streamdeck" homedir "github.com/mitchellh/go-homedir" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/viper" ) var cfgFile string // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "mystreamdeck", Short: "", Long: ``, Run: root, } // Execute . func Execute() { if err := rootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(1) } } func init() { cobra.OnInitialize(initConfig) rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.mystreamdeck.yaml)") } // initConfig reads in config file and ENV variables if set. func initConfig() { if cfgFile != "" { // Use config file from the flag. viper.SetConfigFile(cfgFile) } else { // Find home directory. home, err := homedir.Dir() if err != nil { fmt.Println(err) os.Exit(1) } // Search config in home directory with name ".mystreamdeck" (without extension). viper.AddConfigPath(home) viper.SetConfigName(".mystreamdeck") } viper.AutomaticEnv() // read in environment variables that match // If a config file is found, read it in. if err := viper.ReadInConfig(); err == nil { log.Println("Using config file:", viper.ConfigFileUsed()) } } // RemoteSendCommand . type RemoteSendCommand struct { EntityID string `json:"entity_id"` Device string `json:"device"` Commands []string `json:"command"` NumRepeats int `json:"num_repeats,omitempty"` DelaySecs float64 `json:"delay_secs,omitempty"` } func root(cmd *cobra.Command, args []string) { var wg sync.WaitGroup var err error sd, err := streamdeck.New() if err != nil { log.Printf("unable to get new streamdeck: %+v", err) return } sd.ResetToLogo() // sd.ResetKeyStream() ha, err := homeassistant.NewConnection( viper.GetString("homeassistant.hostname"), viper.GetInt("homeassistant.port"), viper.GetString("homeassistant.access_token"), viper.GetBool("homeassistant.secure"), ) if err != nil { log.Printf("unable to get connection to home assistant: %+v", err) return } b := internal.NewDynamicIconButton( ha, sd, "switch.living_room_tv", internal.ButtonInfo{X: 2, Y: 2}, internal.DynamicIconInfo{ ForState: func(pressed bool, state string) streamdeck.IconButton { log.Println(pressed, state) switch { case pressed && state == "off": return streamdeck.IconButton{ Icon: "television-off", IconColor: color.RGBA{255, 0, 0, 255}, BackgroundColor: color.RGBA{0, 0, 0, 255}, } case pressed && state == "on": return streamdeck.IconButton{ Icon: "television", IconColor: color.RGBA{255, 0, 0, 255}, BackgroundColor: color.RGBA{0, 0, 0, 255}, } case !pressed && state == "off": return streamdeck.IconButton{ Icon: "television-off", IconColor: color.RGBA{150, 150, 150, 255}, BackgroundColor: color.RGBA{0, 0, 0, 255}, } case !pressed && state == "on": return streamdeck.IconButton{ Icon: "television", IconColor: color.RGBA{255, 255, 0, 255}, BackgroundColor: color.RGBA{0, 0, 0, 255}, } default: return streamdeck.IconButton{ Icon: "television-off", IconColor: color.RGBA{150, 150, 150, 255}, BackgroundColor: color.RGBA{0, 0, 0, 255}, } } }, }, internal.ServiceInfo{ Domain: "homeassistant", Service: "toggle", Data: internal.HomeAssistantToggle{EntityID: "switch.living_room_tv"}, }, ) b.InitializeButton() s := internal.NewStaticIconButton( ha, sd, internal.ButtonInfo{X: 0, Y: 2}, internal.StaticIconInfo{ Icon: "volume-plus", FgColor: color.RGBA{150, 150, 150, 255}, BgColor: color.RGBA{0, 0, 0, 255}, PressedFgColor: color.RGBA{255, 0, 0, 255}, PressedBgColor: color.RGBA{0, 0, 0, 255}, }, internal.ServiceInfo{ Domain: "remote", Service: "send_command", Data: RemoteSendCommand{ EntityID: "remote.living_room", Device: "Pioneer AV Receiver", Commands: []string{ "VolumeUp", }, }, }, ) s.InitializeButton() s = internal.NewStaticIconButton( ha, sd, internal.ButtonInfo{X: 0, Y: 1}, internal.StaticIconInfo{ Icon: "volume-minus", FgColor: color.RGBA{150, 150, 150, 255}, BgColor: color.RGBA{0, 0, 0, 255}, PressedFgColor: color.RGBA{255, 0, 0, 255}, PressedBgColor: color.RGBA{0, 0, 0, 255}, }, internal.ServiceInfo{ Domain: "remote", Service: "send_command", Data: RemoteSendCommand{ EntityID: "remote.living_room", Device: "Pioneer AV Receiver", Commands: []string{ "VolumeDown", }, }, }, ) s.InitializeButton() s = internal.NewStaticIconButton( ha, sd, internal.ButtonInfo{X: 0, Y: 0}, internal.StaticIconInfo{ Icon: "volume-mute", FgColor: color.RGBA{150, 150, 150, 255}, BgColor: color.RGBA{0, 0, 0, 255}, PressedFgColor: color.RGBA{255, 0, 0, 255}, PressedBgColor: color.RGBA{0, 0, 0, 255}, }, internal.ServiceInfo{ Domain: "remote", Service: "send_command", Data: RemoteSendCommand{ EntityID: "remote.living_room", Device: "Pioneer AV Receiver", Commands: []string{ "Mute", }, }, }, ) s.InitializeButton() s = internal.NewStaticIconButton( ha, sd, internal.ButtonInfo{X: 1, Y: 2}, internal.StaticIconInfo{ Icon: "pause", FgColor: color.RGBA{150, 150, 150, 255}, BgColor: color.RGBA{0, 0, 0, 255}, PressedFgColor: color.RGBA{255, 0, 0, 255}, PressedBgColor: color.RGBA{0, 0, 0, 255}, }, internal.ServiceInfo{ Domain: "remote", Service: "send_command", Data: RemoteSendCommand{ EntityID: "remote.living_room", Device: "NVIDIA Shield TV", Commands: []string{ "Pause", }, }, }, ) s.InitializeButton() s = internal.NewStaticIconButton( ha, sd, internal.ButtonInfo{X: 1, Y: 1}, internal.StaticIconInfo{ Icon: "skip-forward", FgColor: color.RGBA{150, 150, 150, 255}, BgColor: color.RGBA{0, 0, 0, 255}, PressedFgColor: color.RGBA{255, 0, 0, 255}, PressedBgColor: color.RGBA{0, 0, 0, 255}, }, internal.ServiceInfo{ Domain: "remote", Service: "send_command", Data: RemoteSendCommand{ EntityID: "remote.living_room", Device: "NVIDIA Shield TV", Commands: []string{ "NextTrack", }, }, }, ) s.InitializeButton() s = internal.NewStaticIconButton( ha, sd, internal.ButtonInfo{X: 1, Y: 0}, internal.StaticIconInfo{ Icon: "skip-backward", FgColor: color.RGBA{150, 150, 150, 255}, BgColor: color.RGBA{0, 0, 0, 255}, PressedFgColor: color.RGBA{255, 0, 0, 255}, PressedBgColor: color.RGBA{0, 0, 0, 255}, }, internal.ServiceInfo{ Domain: "remote", Service: "send_command", Data: RemoteSendCommand{ EntityID: "remote.living_room", Device: "NVIDIA Shield TV", Commands: []string{ "PreviousTrack", }, }, }, ) s.InitializeButton() font, err := readFont("/usr/share/fonts/google-droid-sans-mono-fonts/DroidSansMono.ttf") // font, err := readFont("/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf") if err != nil { log.Printf("unable to load font: %+v", err) return } t := internal.NewDynamicIconTextButton( ha, sd, "switch.lamp_near_tv", internal.ButtonInfo{ X: 4, Y: 2, }, internal.DynamicIconTextInfo{ ForState: func(pressed bool, state string) streamdeck.IconTextButton { log.Println(pressed, state) switch { case pressed && state == "off": return streamdeck.IconTextButton{ Icon: "floor-lamp", IconColor: color.RGBA{255, 0, 0, 255}, Text: "Near TV", FontSize: 14, FontColor: color.White, Font: font, BackgroundColor: color.Black, } case pressed && state == "on": return streamdeck.IconTextButton{ Icon: "floor-lamp", IconColor: color.RGBA{255, 0, 0, 255}, Text: "Near TV", FontSize: 14, FontColor: color.White, Font: font, BackgroundColor: color.Black, } case !pressed && state == "off": return streamdeck.IconTextButton{ Icon: "floor-lamp", IconColor: color.Gray{150}, Text: "Near TV", FontSize: 14, FontColor: color.White, Font: font, BackgroundColor: color.Black, } case !pressed && state == "on": return streamdeck.IconTextButton{ Icon: "floor-lamp", IconColor: color.RGBA{255, 255, 0, 255}, Text: "Near TV", FontSize: 14, FontColor: color.White, Font: font, BackgroundColor: color.RGBA{0, 0, 0, 255}, } default: return streamdeck.IconTextButton{ Icon: "floor-lamp", IconColor: color.Gray{150}, Text: "Near TV", FontSize: 14, FontColor: color.White, Font: font, BackgroundColor: color.RGBA{0, 0, 0, 255}, } } }, }, internal.ServiceInfo{ Domain: "homeassistant", Service: "toggle", Data: internal.HomeAssistantToggle{EntityID: "switch.lamp_near_tv"}, }, ) t.InitializeButton() t = internal.NewDynamicIconTextButton( ha, sd, "light.lamp_near_recliner", internal.ButtonInfo{ X: 4, Y: 1, }, internal.DynamicIconTextInfo{ ForState: func(pressed bool, state string) streamdeck.IconTextButton { log.Println(pressed, state) switch { case pressed && state == "off": return streamdeck.IconTextButton{ Icon: "floor-lamp", IconColor: color.RGBA{255, 0, 0, 255}, Text: "Recliner", FontSize: 14, FontColor: color.White, Font: font, BackgroundColor: color.Black, } case pressed && state == "on": return streamdeck.IconTextButton{ Icon: "floor-lamp", IconColor: color.RGBA{255, 0, 0, 255}, Text: "Recliner", FontSize: 14, FontColor: color.White, Font: font, BackgroundColor: color.Black, } case !pressed && state == "off": return streamdeck.IconTextButton{ Icon: "floor-lamp", IconColor: color.Gray{150}, Text: "Recliner", FontSize: 14, FontColor: color.White, Font: font, BackgroundColor: color.Black, } case !pressed && state == "on": return streamdeck.IconTextButton{ Icon: "floor-lamp", IconColor: color.RGBA{255, 255, 0, 255}, Text: "Recliner", FontSize: 14, FontColor: color.White, Font: font, BackgroundColor: color.RGBA{0, 0, 0, 255}, } default: return streamdeck.IconTextButton{ Icon: "floor-lamp", IconColor: color.Gray{150}, Text: "Recliner", FontSize: 14, FontColor: color.White, Font: font, BackgroundColor: color.RGBA{0, 0, 0, 255}, } } }, }, internal.ServiceInfo{ Domain: "homeassistant", Service: "toggle", Data: internal.HomeAssistantToggle{EntityID: "light.lamp_near_recliner"}, }, ) t.InitializeButton() t = internal.NewDynamicIconTextButton( ha, sd, "switch.lamp_near_couch", internal.ButtonInfo{ X: 4, Y: 0, }, internal.DynamicIconTextInfo{ ForState: func(pressed bool, state string) streamdeck.IconTextButton { log.Println(pressed, state) switch { case pressed && state == "off": return streamdeck.IconTextButton{ Icon: "floor-lamp", IconColor: color.RGBA{255, 0, 0, 255}, Text: "Couch", FontSize: 14, FontColor: color.White, Font: font, BackgroundColor: color.Black, } case pressed && state == "on": return streamdeck.IconTextButton{ Icon: "floor-lamp", IconColor: color.RGBA{255, 0, 0, 255}, Text: "Couch", FontSize: 14, FontColor: color.White, Font: font, BackgroundColor: color.Black, } case !pressed && state == "off": return streamdeck.IconTextButton{ Icon: "floor-lamp", IconColor: color.Gray{150}, Text: "Couch", FontSize: 14, FontColor: color.White, Font: font, BackgroundColor: color.Black, } case !pressed && state == "on": return streamdeck.IconTextButton{ Icon: "floor-lamp", IconColor: color.RGBA{255, 255, 0, 255}, Text: "Couch", FontSize: 14, FontColor: color.White, Font: font, BackgroundColor: color.RGBA{0, 0, 0, 255}, } default: return streamdeck.IconTextButton{ Icon: "floor-lamp", IconColor: color.Gray{150}, Text: "Couch", FontSize: 14, FontColor: color.White, Font: font, BackgroundColor: color.RGBA{0, 0, 0, 255}, } } }, }, internal.ServiceInfo{ Domain: "homeassistant", Service: "toggle", Data: internal.HomeAssistantToggle{EntityID: "switch.lamp_near_couch"}, }, ) t.InitializeButton() t = internal.NewDynamicIconTextButton( ha, sd, "switch.lava_lamp", internal.ButtonInfo{ X: 3, Y: 2, }, internal.DynamicIconTextInfo{ ForState: func(pressed bool, state string) streamdeck.IconTextButton { log.Println(pressed, state) switch { case pressed: return streamdeck.IconTextButton{ Icon: "lava-lamp", IconColor: color.RGBA{255, 0, 0, 255}, Text: "Lava Lamp", FontSize: 14, FontColor: color.White, Font: font, BackgroundColor: color.Black, } case !pressed && state == "off": return streamdeck.IconTextButton{ Icon: "lava-lamp", IconColor: color.Gray{150}, Text: "Lava Lamp", FontSize: 14, FontColor: color.White, Font: font, BackgroundColor: color.Black, } case !pressed && state == "on": return streamdeck.IconTextButton{ Icon: "lava-lamp", IconColor: color.RGBA{0, 0, 255, 255}, Text: "Lava Lamp", FontSize: 14, FontColor: color.White, Font: font, BackgroundColor: color.RGBA{0, 0, 0, 255}, } default: return streamdeck.IconTextButton{ Icon: "lava-lamp", IconColor: color.Gray{150}, Text: "Lava Lamp", FontSize: 14, FontColor: color.White, Font: font, BackgroundColor: color.RGBA{0, 0, 0, 255}, } } }, }, internal.ServiceInfo{ Domain: "homeassistant", Service: "toggle", Data: internal.HomeAssistantToggle{EntityID: "switch.lava_lamp"}, }, ) t.InitializeButton() wg.Add(1) go func() { defer wg.Done() err = internal.InitializeState(ha) if err != nil { log.Printf("unable to initalize states: %+v", err) return } err = internal.StartWatchingEvents(ha) if err != nil { log.Printf("unable to start watching states: %+v", err) } }() wg.Add(1) go func() { defer wg.Done() sd.Read() }() wg.Wait() } func readFont(path string) (*truetype.Font, error) { font, err := freetype.ParseFont(dejavusans.TTF) if err != nil { return nil, errors.Wrap(err, "unable to parse font") } // fontFile, err := os.Open(path) // if err != nil { // return nil, errors.Wrapf(err, "unable to open font '%s'", path) // } // defer fontFile.Close() // fontData, err := ioutil.ReadAll(fontFile) // if err != nil { // return nil, errors.Wrapf(err, "unable to read font '%s'", path) // } // font, err := freetype.ParseFont(fontData) // if err != nil { // return nil, errors.Wrapf(err, "unable to parse font '%s'", path) // } return font, nil }