Skip to content

Commit

Permalink
Uploaded files
Browse files Browse the repository at this point in the history
First Release
  • Loading branch information
lordofscripts authored Nov 2, 2023
1 parent 9aead25 commit 1f2446e
Show file tree
Hide file tree
Showing 10 changed files with 1,455 additions and 0 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## Changelog for lordofscripts/govee

---
### 1.0
##### Released on _Wed 1.Nov.2023_ by _lordofscripts_
### First Release!
* Supports queries (-list and -state) and state changes (-on and -off)
* While it seems like an overkill, it uses a _Command Pattern_ to process commands
* Command-line interface application written in GO
674 changes: 674 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

95 changes: 95 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Lord of Scripts(tm)) Govee CLI

This is a handy command-line interface application to list all Govee smart
devices in your home network, as well as control them (On/Off).

The more verbose way requires several **CLI** parameters such as device MAC & Model
in order to control them. Alternatively, you can put all your Govee smart
devices in a configuration file and assign them an easy-to-remember ID (alias).

With this you can use the CLI to perform the actions you desire, or create
desktop shortcuts that invoke the CLI command.

I'm sure you will find this utility quite handy. Feel free to consider a small
donation so that I can continue working on these utilities:

[Buy LostInWriting a coffee!](https://www.buymeacoffee.com/lostinwriting)

## Requirements

- GO v1.20
- github.com/loxhill/go-vee
- Request a Govee API key

## Using Govee API
This small Go app is conveniently named "govee" which is the name of the
executable.

### Configuration File

The configuration file, if present, should be at `~/.config/govee.json` and it
must be in JSON format. It would look like this:

>{
> "version": "0.1",
> "apiKey": "YOUR GOVEE API KEY HERE"
> "devices": [
> {
> "model": "H5083",
> "mac": "DEVICE MAC ADDRESS",
> "alias": "SmartPlug1",
> "location": "Recamara #2"
> }
> ]
>}
The MAC address is a series of 8 hexadecimal pairs which is unique to each
device and looks like this: `AA:BB:CC:DD:EE:FF:00:11`

You will need to request a personal API key to Govee, they are quite quick. But
do remember that it is only for **personal** use, not commercial use! If you
use a configuration file, make sure to put your API key where indicated.
Alternatively, you can put the API key in the `GOVEE_API` environment variable.

### Obtaining a GOVEE API key

The keys are for personal use only, if you keep that in mind and suits your needs,
then all is okay.

- Install the Govee Home app on your mobile. You probably already have since
you have a Govee device. I am using Govee Home app v5.8.30.
- Click on the Cog wheel (`Settings`) on the upper right)
- In the `Settings` page, click on the `Apply for API Key`
- Fill out the requested information (Name, reason, agreement) and send.

You should receive the personal API key within a day.)

### Usage

> govee -list
Uses your WiFi/Internet connection to query your local network and list ALL
Govee smart devices. It will list them giving you their model number (for use
with -model flag), MAC address (-mac flag) and official name.

> govee -mac {MAC_ADDRESS} -model {MODEL_NUMBER} -on
Turns on the device identified by the MAC address and Model number.

> govee -mac {MAC_ADDRESS} -model {MODEL_NUMBER} -off
Turns off the device identified by the MAC address and Model number.

However, it is much easier to assign easy identifications to your device and
even annotate it with their locations within your premises. For that, you need
to use a *configuration file* as described above. Once you do that, you can
easily turn your devices on or off using their alias, which you can remember
without having to look them up. Here are the config-based versions of the
previous two commands:

> govee -id {ALIAS_NAME} -on
Turns on the device identified by the MAC address and Model number. For the
sample configuration above it would become `govee -id SmartPlug1 -on`

> govee -id {ALIAS_NAME} -off
Turns off the device identified by the MAC address and Model number.

-----
https://allmylinks.com/lordofscripts
281 changes: 281 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
/* -----------------------------------------------------------------
* https://allmylinks.com/lordofscripts
* Copyright (C)2023 Lord of Scripts(tm)
* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Utility program that uses GOVEE API to control (On/Off/List/State)
* GOVEE smart devices in your network.
*-----------------------------------------------------------------*/
package main
// https://govee-public.s3.amazonaws.com/developer-docs/GoveeDeveloperAPIReference.pdf

import (
"flag"
"fmt"
"os"
"path"
"strings"

"lordofscripts/govee"
"lordofscripts/govee/util"
veex "github.com/loxhill/go-vee"
)

const (
HOME_ENV = "HOME" // environment var. holding user home directory
MY_CONFIG = ".config/govee.json" // config file relative to HOME_ENV
API_KEY = ""
)

/* ----------------------------------------------------------------
* T y p e s
*-----------------------------------------------------------------*/
type ICommand interface {
execute() error
}

type GoveeCommand struct {
Client *veex.Client
Address string
Model string
}

/* ----------------------------------------------------------------
* T y p e s
* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* OnCommand::GoveeCommand
*-----------------------------------------------------------------*/
type OnCommand struct {
GoveeCommand
}

func newCmdOn(clientPtr *veex.Client, address, model string) *OnCommand {
o := &OnCommand{
GoveeCommand: GoveeCommand{
Client: clientPtr,
Address: address,
Model: model,
},
}
return o
}

// implements ICommand for OnCommand
func (c *OnCommand) execute() error {
controlRequest, _ := c.Client.Device(c.Address, c.Model).TurnOn()
_, err := c.Client.Run(controlRequest)
return err
}

/* ----------------------------------------------------------------
* T y p e s
* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* OffCommand::GoveeCommand
*-----------------------------------------------------------------*/
type OffCommand struct {
GoveeCommand
}

func newCmdOff(clientPtr *veex.Client, address, model string) *OffCommand {
o := &OffCommand{
GoveeCommand: GoveeCommand{
Client: clientPtr,
Address: address,
Model: model,
},
}
return o
}

// implements ICommand for OffCommand
func (c *OffCommand) execute() error {
controlRequest, _ := c.Client.Device(c.Address, c.Model).TurnOff()
_, err := c.Client.Run(controlRequest)
return err
}

/* ----------------------------------------------------------------
* T y p e s
* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* StateCommand::GoveeCommand
*-----------------------------------------------------------------*/

// the State commands obtains the online and powered state of a device
// along its h/w address and model-
type StateCommand struct {
GoveeCommand
}

func newCmdState(clientPtr *veex.Client, address, model string) *StateCommand {
o := &StateCommand{
GoveeCommand: GoveeCommand{
Client: clientPtr,
Address: address,
Model: model,
},
}
return o
}

// implements ICommand for StateCommand
func (c *StateCommand) execute() error {
stateRequest := c.Client.Device(c.Address, c.Model).State()
// {Code:200 Message:Success Data:{Device:D7:B6:60:74:F4:02:D5:A2 Model:H5083 Properties:[map[online:true] map[powerState:off]] Devices:[]}}
rsp, err := c.Client.Run(stateRequest) // govee.GoveeResponse
//fmt.Printf("%+v\n", rsp)
if rsp.Code != 200 {
err = fmt.Errorf("State request failed: %q", rsp.Message)
} else {
fmt.Println("\tMAC :", rsp.Data.Device)
fmt.Println("\tModel :", rsp.Data.Model)
fmt.Println("\tOnline :", rsp.Data.Properties[0]["online"])
fmt.Println("\tPowered:", rsp.Data.Properties[1]["powerState"])
}
return err
}
/* ----------------------------------------------------------------
* T y p e s
* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* ListCommand::GoveeCommand
*-----------------------------------------------------------------*/
type ListCommand struct {
GoveeCommand
}

func newCmdList(clientPtr *veex.Client) *ListCommand {
o := &ListCommand{
GoveeCommand: GoveeCommand{
Client: clientPtr,
Address: "",
Model: "",
},
}
return o
}

// implements ICommand for ListCommand
func (c *ListCommand) execute() error {
listRequest := c.Client.ListDevices()
if response, err := c.Client.Run(listRequest); err != nil {
return err
} else {
devices := response.Devices()
for i,d := range devices {
fmt.Printf("#%d\n\tDevice: %s\n\tModel : %s\n\tType : %s\n", i+1, d.Device, d.Model, d.DeviceName)
}
}
return nil
}

/* ----------------------------------------------------------------
* F u n c t i o n s
*-----------------------------------------------------------------*/

func getHelp() {
fmt.Printf("Govee v%s by Lord of Scripts\n", govee.CURRENT_VERSION)
fmt.Println("Usage:")
fmt.Println(" govee -list")
fmt.Println(" govee -mac {MAC_ADDRESS} -model {MODEL_NUMBER} -on")
fmt.Println(" govee -mac {MAC_ADDRESS} -model {MODEL_NUMBER} -off")
fmt.Println(" govee -mac {MAC_ADDRESS} -model {MODEL_NUMBER} -state")
// these two need a config file with entries
fmt.Println(" govee -id {ALIAS} -on")
fmt.Println(" govee -id {ALIAS} -off")
fmt.Println(" govee -id {ALIAS} -state")
flag.PrintDefaults()
fmt.Println("\t*** ***")
}

/* ----------------------------------------------------------------
* M A I N
*-----------------------------------------------------------------*/
func main() {
fmt.Printf("\t\t../ GoveeLux v%s (c)2023 Lord of Scripts \\..\n", govee.CURRENT_VERSION)
fmt.Println("\t\t https://allmylinks.com/lordofscripts")

var optHelp, optList, optOn, optOff, optState bool
var inDevice, inModel, inAlias string
flag.BoolVar(&optHelp, "help", false, "This help")
flag.BoolVar(&optList, "list", false, "List devices")
flag.BoolVar(&optOn, "on", false, "Turn ON")
flag.BoolVar(&optOff, "off", false, "Turn OFF")
flag.BoolVar(&optState, "state", false, "Request online/power state of device")
flag.StringVar(&inDevice, "mac", "", "Device MAC")
flag.StringVar(&inModel, "model", "", "Device Model")
flag.StringVar(&inAlias, "id", "", "Device alias (derive Model & MAC from this)")
flag.Parse()

// any command given or at least help?
if optHelp || !(optList || optOn || optOff || optState) {
getHelp()
if optHelp {
os.Exit(0)
}
os.Exit(1)
}
// state && on & off are mutually exclusive
if !util.One(optOn, optOff, optState, optList) {
getHelp()
fmt.Println("Decide either -on OR -off OR -state OR -list")
os.Exit(1)
}

// Config
cfgFilename := path.Join(os.Getenv(HOME_ENV), MY_CONFIG)

if len(inAlias) != 0 {
cfg := govee.Read(cfgFilename)
candidates := cfg.Devices.Where(govee.ALIAS, inAlias)
cnt := candidates.Count()
if cnt == 0 {
fmt.Printf("Could not find alias %q in repository\n", inAlias)
os.Exit(2)
}

if cnt > 1 {
fmt.Printf("Found %d entries. Alias not unique, please correct config\n", cnt)
os.Exit(2)
}

fmt.Println("\tFound ", candidates[0], "\n\tAt :", candidates[0].Location)
inDevice = candidates[0].MacAddress
inModel = candidates[0].Model
}

// with STATE, ON & OFF commands DEVICE & MODEL are required
if (optOn || optOff || optState) && ((len(inDevice) == 0) && (len(inModel) == 0)) {
getHelp()
fmt.Println("-dev MAC and -model MODEL options are required!")
os.Exit(1)
} else {
inDevice = strings.ToUpper(inDevice)
inModel = strings.ToUpper(inModel)
}

key := govee.GetAPI(cfgFilename)
if len(key) == 0 {
fmt.Println("You forgot to set your GOVEE API Key!")
os.Exit(2)
}

// TurnOff, TurnOn, SetBrightness, SetColor, SetColorTem
client := veex.New(key)
var command ICommand
switch {
case optList:
command = newCmdList(client)

case optOn:
command = newCmdOn(client, inDevice, inModel)

case optOff:
command = newCmdOff(client, inDevice, inModel)

case optState:
command = newCmdState(client, inDevice, inModel)
}

if err := command.execute(); err != nil {
//fmt.Printf("Error type: %T\nMessage: %s\n%v\n", err, err, err)
fmt.Printf("\tError : %v\n", err)
}
}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module lordofscripts/govee

go 1.21.0

require github.com/loxhill/go-vee v0.0.0-20230825161508-7c8c34d2ee28
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/loxhill/go-vee v0.0.0-20230825161508-7c8c34d2ee28 h1:oSED7BzozltkdBVaMIaQkK5d7/V9avQx/XK3HfV8Aw0=
github.com/loxhill/go-vee v0.0.0-20230825161508-7c8c34d2ee28/go.mod h1:gLqZGKn+W6944XJfI9VastC/QRVTVs+Htd9R8aiE1dQ=
Loading

0 comments on commit 1f2446e

Please sign in to comment.