-
Notifications
You must be signed in to change notification settings - Fork 2
/
commands_standard.go
268 lines (229 loc) · 7.65 KB
/
commands_standard.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
package yeelight
import (
"errors"
"fmt"
"log"
"net"
"time"
)
type standardCommands struct {
commander commander
}
// Prop reads given properties
// Not implemented! TODO: TODO
func (c *standardCommands) Prop(props ...Property) (map[string]interface{}, error) {
var data = make(map[string]interface{})
return data, errors.New("not implemented")
}
// CronAdd sets timer which invokes given CronType operation (power off is only supported)
func (c *standardCommands) CronAdd(jobType CronType, minutes int) error {
if !(jobType == CRON_TYPE_POWER_OFF) {
return errors.New("jobType needs to be 0 (power off/timer)")
}
return c.commander.executeCommand(
partialCommand{"cron_add", params{int(jobType), minutes}},
)
}
// Not implemented! TODO: TODO
func (c *standardCommands) CronGet(jobType CronType) error {
return errors.New("not implemented")
}
// CronDel removes a timer for given CronType operation
func (c *standardCommands) CronDel(jobType CronType) error {
if !(jobType == CRON_TYPE_POWER_OFF) {
return errors.New("jobType needs to be 0 (power off/timer)")
}
return c.commander.executeCommand(
partialCommand{"cron_del", params{int(jobType)}},
)
}
// SetAdjust tunes given AdjustProp in a given Action behavior.
// This method is not very precise, please look for dedicated AdjustXxx functions instead
func (c *standardCommands) SetAdjust(action Action, prop AdjustProp) error {
if prop == ADJUST_PROP_COLOR && action != ADJUST_ACTION_CIRCLE { // edge case from documentation
return errors.New("color adjusting can be only performed with \"circle\" action")
}
return c.commander.executeCommand(
partialCommand{"set_adjust", params{string(action), string(prop)}},
)
}
// AdjustBright adjusts bright, range: -100 - 100
func (c *standardCommands) AdjustBright(percentage, duration int) error {
if percentage < -100 || percentage > 100 {
return errors.New("percentage range must be -100 - 100")
}
return c.commander.executeCommand(
partialCommand{"adjust_bright", params{percentage, duration}},
)
}
// AdjustTemperature adjusts temperature, range: -100 - 100
func (c *standardCommands) AdjustTemperature(percentage, duration int) error {
if percentage < -100 || percentage > 100 {
return errors.New("percentage range must be -100 - 100")
}
return c.commander.executeCommand(
partialCommand{"adjust_ct", params{percentage, duration}},
)
}
// AdjustColor adjusts color, range: -100 - 100
func (c *standardCommands) AdjustColor(percentage, duration int) error {
if percentage < -100 || percentage > 100 {
return errors.New("percentage range must be -100 - 100")
}
return c.commander.executeCommand(
partialCommand{"adjust_color", params{percentage, duration}},
)
}
// SetName sets device name
func (c *standardCommands) SetName(name string) error {
return c.commander.executeCommand(
partialCommand{"set_name", params{name}},
)
}
func findIface(name string) ([]net.Interface, error) {
var ifacesToReturn []net.Interface
interfaces, err := net.Interfaces()
if err != nil {
return []net.Interface{}, fmt.Errorf("iface not found: %v", err)
}
for _, iface := range interfaces {
if iface.Name == name {
ifacesToReturn = append(ifacesToReturn, iface)
}
}
if len(ifacesToReturn) == 0 {
return ifacesToReturn, fmt.Errorf("iface \"%s\" not found", name)
}
return ifacesToReturn, nil
}
// findIPv4Addr finds first valid IPv4 address on given net.Interface
func findIPv4Addr(iface net.Interface) (net.IP, error) {
var retAddr net.IP
addresses, err := iface.Addrs()
if err != nil {
return retAddr, fmt.Errorf("failed to fetch binded addresses on \"%s\" interface: %v", iface.Name, err)
}
for _, addr := range addresses {
ip, _, err := net.ParseCIDR(addr.String())
if err != nil {
continue
}
// check if ip is IPv4 type
if ip.To4() != nil {
return ip, nil
}
}
return net.IP{}, errors.New(fmt.Sprintf("IPv4 not found on \"%s\" interfgace", iface.Name))
}
// StartMusic starts tries to run music mode.
// You can perform operations on returned music object without quota limitations
// Interface name can be passed to select exact interface for music server on first assigned IPv4 address
// (bulb needs to connect to opened socket by client), empty string may be passed ("") for
// trying to connect on first available (up and non-loopback) interface and first assigned IPv4 address
func (c *standardCommands) StartMusic(ifaceName string) (musicSupportedCommands, error) {
// TODO: Check "ignored" error when iptables not realoaded (personal archlinux issue)
var (
ifacesToTry []net.Interface
err error
)
if ifaceName == "" {
log.Printf(
"[music] iface name not specified, trying to bind on " +
"first available up and not loopback (localhost) interface...")
ifacesToTry, err = net.Interfaces()
if err != nil {
return nil, fmt.Errorf("failed to read available interfaces: %v", err)
}
} else {
log.Printf("[music] trying to bind on \"%s\" iface...", ifaceName)
ifacesToTry, err = findIface(ifaceName)
if err != nil {
return nil, fmt.Errorf("[music] initialization failed: %v", err)
}
}
var (
binded bool
bindedIface net.Interface
// As far as yeelight devices mainly supports ipv4 only, I'm assuming IPv4 communication only
bindedIPv4Addr net.IP
bindedPort int
listener net.Listener
)
for _, iface := range ifacesToTry {
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
// Interface is neither up or non-loopback (localhost)
continue
}
bindedIPv4Addr, err = findIPv4Addr(iface)
if err != nil {
// failing find a valid IPv4 address
continue
}
// TODO: subnet validation could be invoked here
listener, bindedPort, err = openSocket(bindedIPv4Addr.String(), 1023, 1<<16-1) // first 1024 ports are root-only
if err != nil {
// port opening failed on 1024-65535 range
continue
}
bindedIface = iface
binded = true
break
}
if !binded {
return nil, fmt.Errorf("failed to bind on any of given interfaces")
}
log.Printf("[music] binded on \"%v\" iface on \"%s\" address on \"%d\" port",
bindedIface.Name, bindedIPv4Addr, bindedPort)
incomingConnection := make(chan net.Conn)
defer close(incomingConnection)
// starting "music server" and waiting for first incoming connection
go func(listener net.Listener) {
log.Printf("[music] Waiting for a device connection...")
conn, err := listener.Accept()
if err != nil {
log.Printf("[music] Device fonnection failed: %v", err)
return
}
log.Printf("[music] Device connected!")
incomingConnection <- conn
var buf = make([]byte, 1)
go func(conn net.Conn) { // not very clever solution but at least something for debugging
_, err = conn.Read(buf)
log.Printf("[music] Device disconnected")
}(conn)
}(listener)
log.Printf("[music] Initializating Music Mode...")
err = c.commander.executeCommand(
partialCommand{"set_music", params{1, bindedIPv4Addr, bindedPort}},
)
if err != nil {
return nil, err
}
log.Printf("[music] Music Mode Initialized!")
select {
case conn := <-incomingConnection:
music := NewMusic(conn)
if music == nil {
return nil, errors.New("[music] Connection failed")
}
return music, nil
case <-time.After(time.Second * 2): // 2 second timeout
log.Println("[music] ")
err := listener.Close()
if err != nil {
log.Printf("[music] failed to close music server: %v", err)
}
return nil, fmt.Errorf("device connection timeout")
}
}
// chooseEffect returns effect string Accordingly to given duration value.
// Chooses between "sudden" and "smooth".
func chooseEffect(duration int) (string, error) {
// if duration != 0 && duration < 30 {
// return "", errors.New("Ooops")
// }
if duration == 0 {
return "sudden", nil
}
return "smooth", nil
}