Skip to content

Commit 13155bc

Browse files
authored
Merge pull request #93 from stahnma/elgato
feat(elgato): Add Elgato controller for ring-light
2 parents c7709f1 + 73f8659 commit 13155bc

File tree

5 files changed

+261
-0
lines changed

5 files changed

+261
-0
lines changed

elgato/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
elgato

elgato/Makefile

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
NAME=elgato
2+
3+
default: build
4+
5+
all: platforms
6+
7+
fmt:
8+
go fmt .
9+
10+
tidy: fmt
11+
go mod tidy
12+
13+
VERSION=$(shell git describe --tags --always --dirty)
14+
COMMIT=$(shell git rev-parse --short HEAD)
15+
BUILDDATE=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ')
16+
17+
build: tidy
18+
go build -ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.buildDate=$(BUILDDATE)" -o $(NAME) .
19+
20+
clean:
21+
rm -rf $(NAME) bin index.html
22+
23+
linux-arm64: tidy
24+
GOOS=linux GOARCH=arm64 go build -ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.buildDate=$(BUILDDATE)" -o bin/$(NAME).linux-arm64 .
25+
26+
linux-amd64: tidy
27+
GOOS=linux GOARCH=amd64 go build -ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.buildDate=$(BUILDDATE)" -o bin/$(NAME).linux-amd64 .
28+
29+
darwin-arm64: tidy
30+
GOOS=darwin GOARCH=arm64 go build -ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.buildDate=$(BUILDDATE)" -o bin/$(NAME).mac-arm64 .
31+
32+
darwin-amd64:
33+
GOOS=darwin GOARCH=amd64 go build -ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.buildDate=$(BUILDDATE)" -o bin/$(NAME).mac-amd64 .
34+
35+
mac-arm64: darwin-arm64
36+
37+
mac-amd64: darwin-amd64
38+
39+
linux: linux-arm64 linux-amd64
40+
41+
mac: darwin-arm64 darwin-amd64
42+
43+
install: build
44+
sudo install -p -m0755 $(NAME) /usr/local/bin
45+
46+
platforms: mac linux

elgato/go.mod

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module github.com/stahnma/mandatoryFun/elgato
2+
3+
go 1.21.4
4+
5+
require github.com/grandcat/zeroconf v1.0.0
6+
7+
require (
8+
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
9+
github.com/miekg/dns v1.1.27 // indirect
10+
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 // indirect
11+
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa // indirect
12+
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe // indirect
13+
)

elgato/go.sum

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
2+
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
3+
github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE=
4+
github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs=
5+
github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM=
6+
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
7+
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
8+
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
9+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
10+
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
11+
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
12+
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
13+
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
14+
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
15+
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
16+
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA=
17+
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
18+
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
19+
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
20+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
21+
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
22+
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M=
23+
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
24+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
25+
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
26+
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

elgato/main.go

+175
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"io/ioutil"
9+
"log"
10+
"net/http"
11+
"os"
12+
"path/filepath"
13+
"time"
14+
15+
"github.com/grandcat/zeroconf"
16+
)
17+
18+
const elgatoAPIPath = "/elgato/lights"
19+
20+
var (
21+
version string
22+
commit string
23+
buildDate string
24+
)
25+
26+
type LightState struct {
27+
Lights []struct {
28+
On int `json:"on"`
29+
} `json:"lights"`
30+
}
31+
32+
// findElgatoLight discovers the first Elgato Light on the local network using mDNS.
33+
// It returns the IP address of the found light or an error if no light is found.
34+
func findElgatoLight() (string, error) {
35+
resolver, err := zeroconf.NewResolver(nil)
36+
if err != nil {
37+
return "", fmt.Errorf("failed to initialize resolver: %w", err)
38+
}
39+
40+
entries := make(chan *zeroconf.ServiceEntry)
41+
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
42+
defer cancel()
43+
44+
go func() {
45+
if err := resolver.Browse(ctx, "_elg._tcp", "local.", entries); err != nil {
46+
log.Printf("failed to browse mDNS: %v", err)
47+
}
48+
}()
49+
50+
for entry := range entries {
51+
if len(entry.AddrIPv4) > 0 {
52+
return entry.AddrIPv4[0].String(), nil
53+
}
54+
}
55+
56+
return "", fmt.Errorf("no Elgato light found")
57+
}
58+
59+
// getCurrentState retrieves the current on/off state of the Elgato Light.
60+
// It returns 1 if the light is on, 0 if it is off, or an error if the state cannot be retrieved.
61+
func getCurrentState(ip string) (int, error) {
62+
url := fmt.Sprintf("http://%s:9123%s", ip, elgatoAPIPath)
63+
64+
resp, err := http.Get(url)
65+
if err != nil {
66+
return 0, fmt.Errorf("failed to get current state: %w", err)
67+
}
68+
defer resp.Body.Close()
69+
70+
if resp.StatusCode != http.StatusOK {
71+
return 0, fmt.Errorf("unexpected response: %s", resp.Status)
72+
}
73+
74+
body, err := ioutil.ReadAll(resp.Body)
75+
if err != nil {
76+
return 0, fmt.Errorf("failed to read response body: %w", err)
77+
}
78+
79+
var state LightState
80+
if err := json.Unmarshal(body, &state); err != nil {
81+
return 0, fmt.Errorf("failed to parse JSON: %w", err)
82+
}
83+
84+
if len(state.Lights) > 0 {
85+
return state.Lights[0].On, nil
86+
}
87+
88+
return 0, fmt.Errorf("no light state data available")
89+
}
90+
91+
// toggleLight changes the Elgato Light's state from on to off or vice versa.
92+
// It fetches the current state, determines the opposite state, and sends a request to update it.
93+
func toggleLight(ip string) error {
94+
currentState, err := getCurrentState(ip)
95+
if err != nil {
96+
return fmt.Errorf("failed to get current state: %w", err)
97+
}
98+
99+
newState := 1 - currentState
100+
101+
url := fmt.Sprintf("http://%s:9123%s", ip, elgatoAPIPath)
102+
payload := LightState{
103+
Lights: []struct {
104+
On int `json:"on"`
105+
}{
106+
{On: newState},
107+
},
108+
}
109+
110+
body, err := json.Marshal(payload)
111+
if err != nil {
112+
return fmt.Errorf("failed to marshal JSON: %w", err)
113+
}
114+
115+
req, err := http.NewRequest("PUT", url, bytes.NewBuffer(body))
116+
if err != nil {
117+
return fmt.Errorf("failed to create request: %w", err)
118+
}
119+
req.Header.Set("Content-Type", "application/json")
120+
121+
client := &http.Client{Timeout: 3 * time.Second}
122+
resp, err := client.Do(req)
123+
if err != nil {
124+
return fmt.Errorf("failed to send request: %w", err)
125+
}
126+
defer resp.Body.Close()
127+
128+
if resp.StatusCode != http.StatusOK {
129+
return fmt.Errorf("unexpected response: %s", resp.Status)
130+
}
131+
132+
fmt.Printf("Light toggled to %s\n", map[int]string{0: "off", 1: "on"}[newState])
133+
return nil
134+
}
135+
136+
// printHelp displays the usage instructions for the program.
137+
func printHelp(programName string) {
138+
fmt.Printf(`Usage: %s [option]
139+
140+
This program finds the first Elgato Light on your network and toggles its state.
141+
142+
Options:
143+
--help Show this help message.
144+
--version Show program version.
145+
146+
Example usage:
147+
%s Toggles the light on or off
148+
%s --help Shows this message
149+
`, programName, programName, programName)
150+
}
151+
152+
// main is the entry point of the program. It detects Elgato Lights and toggles their state.
153+
func main() {
154+
programName := filepath.Base(os.Args[0])
155+
156+
if len(os.Args) > 1 {
157+
switch os.Args[1] {
158+
case "--help":
159+
printHelp(programName)
160+
return
161+
case "--version":
162+
fmt.Printf("Version: %s\nCommit: %s\nBuild Date: %s\n", version, commit, buildDate)
163+
return
164+
}
165+
}
166+
167+
ip, err := findElgatoLight()
168+
if err != nil {
169+
log.Fatalf("Error: %v", err)
170+
}
171+
172+
if err := toggleLight(ip); err != nil {
173+
log.Fatalf("Failed to toggle light: %v", err)
174+
}
175+
}

0 commit comments

Comments
 (0)