Skip to content

Commit 5df2e23

Browse files
committed
Initial commit
0 parents  commit 5df2e23

File tree

6 files changed

+311
-0
lines changed

6 files changed

+311
-0
lines changed

.gitignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Binaries for programs and plugins
2+
picoleaf
3+
*.exe
4+
*.exe~
5+
*.dll
6+
*.so
7+
*.dylib
8+
9+
# Test binary, built with `go test -c`
10+
*.test
11+
12+
# Output of the go coverage tool, specifically when used with LiteIDE
13+
*.out
14+
15+
# Dependency directories (remove the comment below to include it)
16+
# vendor/

LICENSE

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
Copyright 2021 Paul Rosania
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of
4+
this software and associated documentation files (the "Software"), to deal in
5+
the Software without restriction, including without limitation the rights to
6+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7+
the Software, and to permit persons to whom the Software is furnished to do so,
8+
subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Picoleaf
2+
3+
Picoleaf is a tiny CLI tool for controlling Nanoleaf.

go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module github.com/paulrosania/picoleaf
2+
3+
go 1.16
4+
5+
require gopkg.in/ini.v1 v1.62.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
2+
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=

main.go

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"flag"
7+
"fmt"
8+
"io/ioutil"
9+
"log"
10+
"net/http"
11+
"os"
12+
"os/user"
13+
"path/filepath"
14+
15+
"gopkg.in/ini.v1"
16+
)
17+
18+
const defaultConfigFile = ".picoleafrc"
19+
20+
var verbose = flag.Bool("v", false, "Verbose")
21+
22+
// Client is a Nanoleaf REST API client.
23+
type Client struct {
24+
Host string
25+
Token string
26+
27+
client http.Client
28+
}
29+
30+
// Get performs a GET request.
31+
func (c Client) Get(path string) string {
32+
if *verbose {
33+
fmt.Println("\nGET", path)
34+
}
35+
36+
url := c.Endpoint(path)
37+
req, err := http.NewRequest(http.MethodGet, url, nil)
38+
if err != nil {
39+
log.Fatal(err)
40+
}
41+
42+
req.Header.Set("Accept", "application/json")
43+
44+
res, err := c.client.Do(req)
45+
if err != nil {
46+
log.Fatal(err)
47+
}
48+
49+
if res.Body != nil {
50+
defer res.Body.Close()
51+
}
52+
53+
body, err := ioutil.ReadAll(res.Body)
54+
if err != nil {
55+
log.Fatal(err)
56+
}
57+
58+
if *verbose {
59+
fmt.Println("<===", string(body))
60+
}
61+
return string(body)
62+
}
63+
64+
// Put performs a PUT request.
65+
func (c Client) Put(path string, body []byte) {
66+
if *verbose {
67+
fmt.Println("PUT", path)
68+
fmt.Println("===>", string(body))
69+
}
70+
71+
url := c.Endpoint(path)
72+
req, err := http.NewRequest(http.MethodPut, url, nil)
73+
if err != nil {
74+
log.Fatal(err)
75+
}
76+
77+
req.Header.Set("Content-Type", "application/json")
78+
req.Header.Set("Accept", "application/json")
79+
80+
req.Body = ioutil.NopCloser(bytes.NewReader(body))
81+
82+
res, err := c.client.Do(req)
83+
if err != nil {
84+
log.Fatal(err)
85+
}
86+
87+
if res.Body != nil {
88+
defer res.Body.Close()
89+
}
90+
}
91+
92+
// Endpoint returns the full URL for an API endpoint.
93+
func (c Client) Endpoint(path string) string {
94+
return fmt.Sprintf("http://%s/api/v1/%s/%s", c.Host, c.Token, path)
95+
}
96+
97+
// ListEffects returns an array of effect names.
98+
func (c Client) ListEffects() ([]string, error) {
99+
body := c.Get("effects/effectsList")
100+
var list []string
101+
err := json.Unmarshal([]byte(body), &list)
102+
return list, err
103+
}
104+
105+
// SelectEffect activates the specified effect.
106+
func (c Client) SelectEffect(name string) error {
107+
req := EffectsSelectRequest{
108+
Select: name,
109+
}
110+
bytes, err := json.Marshal(req)
111+
if err != nil {
112+
return err
113+
}
114+
115+
c.Put("effects/select", bytes)
116+
return nil
117+
}
118+
119+
// BrightnessProperty represents the brightness of the Nanoleaf.
120+
type BrightnessProperty struct {
121+
Value int16 `json:"value"`
122+
Duration int16 `json:"duration,omitempty"`
123+
}
124+
125+
// ColorTemperatureProperty represents the color temperature of the Nanoleaf.
126+
type ColorTemperatureProperty struct {
127+
Value int16 `json:"value"`
128+
}
129+
130+
// HueProperty represents the hue of the Nanoleaf.
131+
type HueProperty struct {
132+
Value int16 `json:"value"`
133+
}
134+
135+
// OnProperty represents the power state of the Nanoleaf.
136+
type OnProperty struct {
137+
Value bool `json:"value"`
138+
}
139+
140+
// SaturationProperty represents the saturation of the Nanoleaf.
141+
type SaturationProperty struct {
142+
Value int16 `json:"value"`
143+
}
144+
145+
// State represents a Nanoleaf state.
146+
type State struct {
147+
On *OnProperty `json:"on,omitempty"`
148+
Brightness *BrightnessProperty `json:"brightness,omitempty"`
149+
ColorTemperature *ColorTemperatureProperty `json:"ct,omitempty"`
150+
Hue *HueProperty `json:"hue,omitempty"`
151+
Saturation *SaturationProperty `json:"sat,omitempty"`
152+
}
153+
154+
// EffectsSelectRequest represents a JSON PUT body for `effects/select`.
155+
type EffectsSelectRequest struct {
156+
Select string `json:"select"`
157+
}
158+
159+
func main() {
160+
flag.Parse()
161+
162+
usr, err := user.Current()
163+
if err != nil {
164+
fmt.Printf("Failed to fetch current user: %v", err)
165+
os.Exit(1)
166+
}
167+
dir := usr.HomeDir
168+
configFilePath := filepath.Join(dir, defaultConfigFile)
169+
170+
cfg, err := ini.Load(configFilePath)
171+
if err != nil {
172+
fmt.Printf("Failed to read file: %v", err)
173+
os.Exit(1)
174+
}
175+
176+
client := Client{
177+
Host: cfg.Section("").Key("host").String(),
178+
Token: cfg.Section("").Key("access_token").String(),
179+
}
180+
181+
if *verbose {
182+
fmt.Printf("Host: %s\n\n", client.Host)
183+
}
184+
185+
if flag.NArg() > 0 {
186+
cmd := flag.Arg(0)
187+
switch cmd {
188+
case "off":
189+
state := State{
190+
On: &OnProperty{false},
191+
}
192+
bytes, err := json.Marshal(state)
193+
if err != nil {
194+
fmt.Printf("Failed to marshal JSON: %v", err)
195+
os.Exit(1)
196+
}
197+
client.Put("state", bytes)
198+
case "on":
199+
state := State{
200+
On: &OnProperty{true},
201+
}
202+
bytes, err := json.Marshal(state)
203+
if err != nil {
204+
fmt.Printf("Failed to marshal JSON: %v", err)
205+
os.Exit(1)
206+
}
207+
client.Put("state", bytes)
208+
case "white":
209+
state := State{
210+
ColorTemperature: &ColorTemperatureProperty{6500},
211+
}
212+
bytes, err := json.Marshal(state)
213+
if err != nil {
214+
fmt.Printf("Failed to marshal JSON: %v", err)
215+
os.Exit(1)
216+
}
217+
client.Put("state", bytes)
218+
case "red":
219+
state := State{
220+
Brightness: &BrightnessProperty{60, 0},
221+
Hue: &HueProperty{0},
222+
Saturation: &SaturationProperty{100},
223+
}
224+
bytes, err := json.Marshal(state)
225+
if err != nil {
226+
fmt.Printf("Failed to marshal JSON: %v", err)
227+
os.Exit(1)
228+
}
229+
client.Put("state", bytes)
230+
case "effect":
231+
doEffectCommand(client, flag.Args()[1:])
232+
}
233+
}
234+
}
235+
236+
func doEffectCommand(client Client, args []string) {
237+
if len(args) < 1 {
238+
fmt.Println("usage: picoleaf effect list")
239+
fmt.Println(" picoleaf effect select <name>")
240+
os.Exit(1)
241+
}
242+
243+
command := args[0]
244+
switch command {
245+
case "list":
246+
list, err := client.ListEffects()
247+
if err != nil {
248+
fmt.Printf("Failed retrieve effects list: %v", err)
249+
os.Exit(1)
250+
}
251+
for _, name := range list {
252+
fmt.Println(name)
253+
}
254+
case "select":
255+
if len(args) != 2 {
256+
fmt.Println("usage: picoleaf effect select <name>")
257+
os.Exit(1)
258+
}
259+
260+
name := args[1]
261+
err := client.SelectEffect(name)
262+
if err != nil {
263+
fmt.Printf("Failed to select effect: %v", err)
264+
os.Exit(1)
265+
}
266+
}
267+
}

0 commit comments

Comments
 (0)