-
Notifications
You must be signed in to change notification settings - Fork 2
/
client.go
920 lines (760 loc) · 20.3 KB
/
client.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
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
package main
import (
"bufio"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"fmt"
"os"
"path"
"strings"
"time"
"github.com/google/uuid"
"github.com/mitchellh/mapstructure"
)
const controlPort = 8081
const pairingPort = 8083
// Client is a Lutron Caséta LEAP API client.
type Client struct {
Host string
CACertPath string
ClientCertPath string
ClientKeyPath string
Verbose bool
conn *tls.Conn
r *bufio.Reader
seqNo int // instead of UUIDs
}
type Request struct {
CommuniqueType string
Header RequestHeader
Body interface{} `json:",omitempty"`
}
type RequestHeader struct {
ClientTag string `json:",omitempty"`
RequestType string `json:",omitempty"`
URL string `json:"Url,omitempty"`
}
type Response struct {
CommuniqueType string
Header ResponseHeader
Body map[string]any
}
type ResponseHeader struct {
ClientTag string
MessageBodyType string
StatusCode string
URL string `json:"Url"`
}
type HrefObject struct {
Href string `json:"href"`
}
func (c Client) loadClientCertificate() (tls.Certificate, error) {
clientCert, err := os.ReadFile(c.ClientCertPath)
if err != nil {
return tls.Certificate{}, err
}
clientKey, err := os.ReadFile(c.ClientKeyPath)
if err != nil {
return tls.Certificate{}, err
}
cert, err := tls.X509KeyPair([]byte(clientCert), []byte(clientKey))
if err != nil {
return tls.Certificate{}, err
}
return cert, nil
}
func (c *Client) dial() error {
cert, err := c.loadClientCertificate()
if err != nil {
return err
}
c.conn, err = tls.Dial("tcp", fmt.Sprintf("%s:%d", c.Host, controlPort), &tls.Config{
InsecureSkipVerify: true,
Certificates: []tls.Certificate{cert},
})
if err != nil {
return err
}
c.r = bufio.NewReader(c.conn)
return nil
}
func (c *Client) dialPairing() error {
cert, err := c.loadPairingCertificate()
if err != nil {
return err
}
c.conn, err = tls.Dial("tcp", fmt.Sprintf("%s:%d", c.Host, pairingPort), &tls.Config{
InsecureSkipVerify: true,
Certificates: []tls.Certificate{cert},
})
if err != nil {
return err
}
c.r = bufio.NewReader(c.conn)
return nil
}
func (c *Client) Close() error {
return c.conn.Close()
}
func (c *Client) generateClientTag() string {
return uuid.NewString()
}
func (c *Client) send(message []byte) error {
if c.Verbose {
os.Stderr.WriteString(fmt.Sprintln("===>", string(message)))
}
_, err := c.conn.Write(message)
if err != nil {
return err
}
_, err = c.conn.Write([]byte("\n"))
if err != nil {
return err
}
return nil
}
func (c *Client) readLine() (string, error) {
line, err := c.r.ReadString('\n')
if err != nil {
return line, err
}
if c.Verbose {
os.Stderr.WriteString(fmt.Sprintln("<===", strings.TrimRight(line, "\n")))
}
return line, nil
}
func (c *Client) loadPairingCertificate() (tls.Certificate, error) {
const clientCert = `-----BEGIN CERTIFICATE-----
MIIECjCCAvKgAwIBAgIBAzANBgkqhkiG9w0BAQ0FADCBlzELMAkGA1UEBhMCVVMx
FTATBgNVBAgTDFBlbm5zeWx2YW5pYTElMCMGA1UEChMcTHV0cm9uIEVsZWN0cm9u
aWNzIENvLiwgSW5jLjEUMBIGA1UEBxMLQ29vcGVyc2J1cmcxNDAyBgNVBAMTK0Nh
c2V0YSBMb2NhbCBBY2Nlc3MgUHJvdG9jb2wgQ2VydCBBdXRob3JpdHkwHhcNMTUx
MDMxMDAwMDAwWhcNMzUxMDMxMDAwMDAwWjB+MQswCQYDVQQGEwJVUzEVMBMGA1UE
CBMMUGVubnN5bHZhbmlhMSUwIwYDVQQKExxMdXRyb24gRWxlY3Ryb25pY3MgQ28u
LCBJbmMuMRQwEgYDVQQHEwtDb29wZXJzYnVyZzEbMBkGA1UEAxMSQ2FzZXRhIEFw
cGxpY2F0aW9uMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyAOELqTw
WNkF8ofSYJ9QkOHAYMmkVSRjVvZU2AqFfaZYCfWLoors7EBeQrsuGyojqxCbtRUd
l2NQrkPrGVw9cp4qsK54H8ntVadNsYi7KAfDW8bHQNf3hzfcpe8ycXcdVPZram6W
pM9P7oS36jV2DLU59A/OGkcO5AkC0v5ESqzab3qaV3ZvELP6qSt5K4MaJmm8lZT2
6deHU7Nw3kR8fv41qAFe/B0NV7IT+hN+cn6uJBxG5IdAimr4Kl+vTW9tb+/Hh+f+
pQ8EzzyWyEELRp2C72MsmONarnomei0W7dVYbsgxUNFXLZiXBdtNjPCMv1u6Znhm
QMIu9Fhjtz18LwIDAQABo3kwdzAJBgNVHRMEAjAAMB0GA1UdDgQWBBTiN03yqw/B
WK/jgf6FNCZ8D+SgwDAfBgNVHSMEGDAWgBSB7qznOajKywOtZypVvV7ECAsgZjAL
BgNVHQ8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMA0GCSqG
SIb3DQEBDQUAA4IBAQABdgPkGvuSBCwWVGO/uzFEIyRius/BF/EOZ7hMuZluaF05
/FT5PYPWg+UFPORUevB6EHyfezv+XLLpcHkj37sxhXdDKB4rrQPNDY8wzS9DAqF4
WQtGMdY8W9z0gDzajrXRbXkYLDEXnouUWA8+AblROl1Jr2GlUsVujI6NE6Yz5JcJ
zDLVYx7pNZkhYcmEnKZ30+ICq6+0GNKMW+irogm1WkyFp4NHiMCQ6D2UMAIMfeI4
xsamcaGquzVMxmb+Py8gmgtjbpnO8ZAHV6x3BG04zcaHRDOqyA4g+Xhhbxp291c8
B31ZKg0R+JaGyy6ZpE5UPLVyUtLlN93V2V8n66kR
-----END CERTIFICATE-----`
const clientKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAyAOELqTwWNkF8ofSYJ9QkOHAYMmkVSRjVvZU2AqFfaZYCfWL
oors7EBeQrsuGyojqxCbtRUdl2NQrkPrGVw9cp4qsK54H8ntVadNsYi7KAfDW8bH
QNf3hzfcpe8ycXcdVPZram6WpM9P7oS36jV2DLU59A/OGkcO5AkC0v5ESqzab3qa
V3ZvELP6qSt5K4MaJmm8lZT26deHU7Nw3kR8fv41qAFe/B0NV7IT+hN+cn6uJBxG
5IdAimr4Kl+vTW9tb+/Hh+f+pQ8EzzyWyEELRp2C72MsmONarnomei0W7dVYbsgx
UNFXLZiXBdtNjPCMv1u6ZnhmQMIu9Fhjtz18LwIDAQABAoIBAQCXDtDNyZQcBgwP
17RzdN8MDPOWJbQO+aRtES2S3J9k/jSPkPscj3/QDe0iyOtRaMn3cFuor4HhzAgr
FPCB/sAJyJrFRX9DwuWUQv7SjkmLOhG5Rq9FsdYoMXBbggO+3g8xE8qcX1k2r7vW
kDW2lRnLDzPtt+IYxoHgh02yvIYnPn1VLuryM0+7eUrTVmdHQ1IGS5RRAGvtoFjf
4QhkkwLzZzCBly/iUDtNiincwRx7wUG60c4ZYu/uBbdJKT+8NcDLnh6lZyJIpGns
jjZvvYA9kgCB2QgQ0sdvm0rA31cbc72Y2lNdtE30DJHCQz/K3X7T0PlfR191NMiX
E7h2I/oBAoGBAPor1TqsQK0tT5CftdN6j49gtHcPXVoJQNhPyQldKXADIy8PVGnn
upG3y6wrKEb0w8BwaZgLAtqOO/TGPuLLFQ7Ln00nEVsCfWYs13IzXjCCR0daOvcF
3FCb0IT/HHym3ebtk9gvFY8Y9AcV/GMH5WkAufWxAbB7J82M//afSghPAoGBAMys
g9D0FYO/BDimcBbUBpGh7ec+XLPaB2cPM6PtXzMDmkqy858sTNBLLEDLl+B9yINi
FYcxpR7viNDAWtilVGKwkU3hM514k+xrEr7jJraLzd0j5mjp55dnmH0MH0APjEV0
qum+mIJmWXlkfKKIiIDgr6+FwIiF5ttSbX1NwnYhAoGAMRvjqrXfqF8prEk9xzra
7ZldM7YHbEI+wXfADh+En+FtybInrvZ3UF2VFMIQEQXBW4h1ogwfTkn3iRBVje2x
v4rHRbzykjwF48XPsTJWPg2E8oPK6Wz0F7rOjx0JOYsEKm3exORRRhru5Gkzdzk4
lok29/z8SOmUIayZHo+cV88CgYEAgPsmhoOLG19A9cJNWNV83kHBfryaBu0bRSMb
U+6+05MtpG1pgaGVNp5o4NxsdZhOyB0DnBL5D6m7+nF9zpFBwH+s0ftdX5sg/Rfs
1Eapmtg3f2ikRvFAdPVf7024U9J4fzyqiGsICQUe1ZUxxetsumrdzCrpzh80AHrN
bO2X4oECgYEAxoVXNMdFH5vaTo3X/mOaCi0/j7tOgThvGh0bWcRVIm/6ho1HXk+o
+kY8ld0vCa7VvqT+iwPt+7x96qesVPyWQN3+uLz9oL3hMOaXCpo+5w8U2Qxjinod
uHnNjMTXCVxNy4tkARwLRwI+1aV5PMzFSi+HyuWmBaWOe19uz3SFbYs=
-----END RSA PRIVATE KEY-----`
cert, err := tls.X509KeyPair([]byte(clientCert), []byte(clientKey))
if err != nil {
return tls.Certificate{}, err
}
return cert, nil
}
// Pair pairs with a Lutron Caséta LEAP controller. This requires the user to
// press the pairing button on the controller. After pairing, the client
// certificate is written to the config file.
func (c *Client) Pair() error {
err := c.dialPairing()
if err != nil {
return err
}
// May as well clean up, since the connection can't be reused due to
// the deadline
defer c.Close()
// NOTE(ptr): Setting a deadline prevents the connection from being
// reused
err = c.conn.SetDeadline(time.Now().Add(2 * time.Minute))
if err != nil {
return err
}
type PairRequestParameters struct {
CSR string
DeviceUID string
DisplayName string
Role string
}
type PairRequestBody struct {
CommandType string
Parameters PairRequestParameters
}
type PairRequest struct {
Body PairRequestBody
Header RequestHeader
}
clientCertDir := path.Dir(c.ClientCertPath)
err = os.MkdirAll(clientCertDir, 0755)
if err != nil {
return err
}
clientKeyDir := path.Dir(c.ClientKeyPath)
err = os.MkdirAll(clientKeyDir, 0755)
if err != nil {
return err
}
caCertDir := path.Dir(c.ClientCertPath)
err = os.MkdirAll(caCertDir, 0755)
if err != nil {
return err
}
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return err
}
// TODO: configure file path
w, err := os.OpenFile(c.ClientKeyPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
if err != nil {
return err
}
err = pem.Encode(w, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
if err != nil {
return err
}
csrCert, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{
SignatureAlgorithm: x509.SHA256WithRSA,
Subject: pkix.Name{
CommonName: "tron",
},
}, priv)
if err != nil {
return err
}
csr := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE REQUEST",
Bytes: csrCert,
})
fmt.Println("Push the button on the back of your controller...")
line, err := c.readLine()
if err != nil {
return err
}
req := PairRequest{
Header: RequestHeader{
RequestType: "Execute",
URL: "/pair",
ClientTag: "pair",
},
Body: PairRequestBody{
CommandType: "CSR",
Parameters: PairRequestParameters{
CSR: string(csr),
DeviceUID: "000000000000",
DisplayName: "tron",
Role: "Admin",
},
},
}
msg, err := json.Marshal(req)
if err != nil {
return err
}
err = c.send(msg)
if err != nil {
return err
}
line, err = c.readLine()
if err != nil {
return err
}
type PairResponse struct {
Body struct {
SigningResult struct {
Certificate string
RootCertificate string
}
}
}
var res PairResponse
err = json.Unmarshal([]byte(line), &res)
if err != nil {
return err
}
err = os.WriteFile(c.ClientCertPath, []byte(res.Body.SigningResult.Certificate), 0644)
if err != nil {
return err
}
err = os.WriteFile(c.CACertPath, []byte(res.Body.SigningResult.RootCertificate), 0644)
if err != nil {
return err
}
return nil
}
// Get sends a `ReadRequest` communique to the controller.
func (c *Client) Get(path string) (map[string]any, error) {
fail := func(err error) (map[string]any, error) { return map[string]any{}, err }
err := c.dial()
if err != nil {
return fail(err)
}
defer c.Close()
tag := c.generateClientTag()
req := Request{
CommuniqueType: "ReadRequest",
Header: RequestHeader{
ClientTag: tag,
URL: path,
},
}
msg, err := json.Marshal(req)
if err != nil {
return fail(err)
}
err = c.send(msg)
if err != nil {
return fail(err)
}
for {
line, err := c.readLine()
if err != nil {
return fail(err)
}
var res Response
err = json.Unmarshal([]byte(line), &res)
if err != nil {
return fail(err)
}
if res.CommuniqueType == "ExceptionResponse" && res.Header.ClientTag == tag {
return fail(fmt.Errorf("received %s: %s", res.Header.StatusCode, res.Body["Message"]))
}
if res.CommuniqueType == "ReadResponse" && res.Header.ClientTag == tag {
if res.Header.StatusCode == "200 OK" {
return res.Body, nil
} else {
return fail(fmt.Errorf("received %s status", res.Header.StatusCode))
}
}
}
}
// Post sends a `CreateRequest` communique to the controller.
func (c *Client) Post(path string, payload any) (map[string]any, error) {
fail := func(err error) (map[string]any, error) { return map[string]any{}, err }
err := c.dial()
if err != nil {
return fail(err)
}
defer c.Close()
tag := c.generateClientTag()
req := Request{
CommuniqueType: "CreateRequest",
Header: RequestHeader{
ClientTag: tag,
URL: path,
},
Body: payload,
}
msg, err := json.Marshal(req)
if err != nil {
return fail(err)
}
err = c.send(msg)
if err != nil {
return fail(err)
}
for {
line, err := c.readLine()
if err != nil {
return fail(err)
}
var res Response
err = json.Unmarshal([]byte(line), &res)
if err != nil {
return fail(err)
}
if res.CommuniqueType == "ExceptionResponse" && res.Header.ClientTag == tag {
return fail(fmt.Errorf("received %s: %s", res.Header.StatusCode, res.Body["Message"]))
}
if res.CommuniqueType == "CreateResponse" && res.Header.ClientTag == tag {
if res.Header.StatusCode == "201 Created" {
return res.Body, nil
} else {
return fail(fmt.Errorf("received %s status", res.Header.StatusCode))
}
}
}
}
type PingResponseBody struct {
PingResponse PingResponse
}
type PingResponse struct {
LEAPVersion float32
}
// Ping sends a `ping` request to the controller. If no error is returned, the
// controller responded with a 200 OK status.
func (c *Client) Ping() (PingResponse, error) {
body, err := c.Get("/server/1/status/ping")
if err != nil {
return PingResponse{}, err
}
var res PingResponseBody
err = mapstructure.Decode(body, &res)
if err != nil {
return PingResponse{}, err
}
return res.PingResponse, nil
}
type DeviceDefinition struct {
Href string `json:"href"`
DeviceType string
ModelNumber string
SerialNumber int
Name string
FullyQualifiedName []string
AddressedState string
AssociatedArea HrefObject
ButtonGroups []HrefObject
DeviceRules []HrefObject
LinkNodes []HrefObject
LocalZones []HrefObject
Parent HrefObject
}
type OneDeviceDefinition struct {
Device DeviceDefinition
}
type MultipleDeviceDefinition struct {
Devices []DeviceDefinition
}
// Devices gets the list of devices this controller knows about.
func (c *Client) Device(id string) (DeviceDefinition, error) {
body, err := c.Get(fmt.Sprintf("/device/%s", id))
if err != nil {
return DeviceDefinition{}, err
}
var res OneDeviceDefinition
err = mapstructure.Decode(body, &res)
if err != nil {
return DeviceDefinition{}, err
}
return res.Device, nil
}
// Devices gets the list of devices this controller knows about.
func (c *Client) Devices() ([]DeviceDefinition, error) {
body, err := c.Get("/device")
if err != nil {
return []DeviceDefinition{}, err
}
var res MultipleDeviceDefinition
err = mapstructure.Decode(body, &res)
if err != nil {
return []DeviceDefinition{}, err
}
return res.Devices, nil
}
type MultipleServerDefinition struct {
Servers []ServerDefinition
}
type ServerDefinition struct {
Type string
Href string `json:"href"`
ProtocolVersion string
EnableState string
Endpoints []struct {
Port int
Protocol string
AssociatedNetworkInterfaces any
}
LEAPProperties struct {
PairingList HrefObject
}
NetworkInterfaces []HrefObject
}
// Servers gets the list of servers this controller knows about. Typically,
// this will just return a single entry for the controller we are connected to.
func (c *Client) Servers() ([]ServerDefinition, error) {
body, err := c.Get("/server")
if err != nil {
return []ServerDefinition{}, err
}
var res MultipleServerDefinition
err = mapstructure.Decode(body, &res)
if err != nil {
return []ServerDefinition{}, err
}
return res.Servers, nil
}
type OneServerDefinition struct {
Server ServerDefinition
}
// Server gets information about the specified server.
func (c *Client) Server(id string) (ServerDefinition, error) {
body, err := c.Get(fmt.Sprintf("/server/%s", id))
if err != nil {
return ServerDefinition{}, err
}
var res OneServerDefinition
err = mapstructure.Decode(body, &res)
if err != nil {
return ServerDefinition{}, err
}
return res.Server, nil
}
type MultipleServiceDefinition struct {
Services []ServiceDefinition
}
type ServiceProperties struct {
// Common properties
DataSummary HrefObject
Errors []struct {
ErrorCode int
Details string
}
// AutoProgrammer-specific
EnabledState string
// HomeKit-specific
BonjourServiceName string
MaxAssociations int
// Sonos-specific
FavoriteHousehold SonosHousehold
Households []SonosHousehold
}
type SonosHousehold HrefObject
type ServiceDefinition struct {
Href string `json:"href"`
Type string
AlexaProperties ServiceProperties
AutoProgrammerProperties ServiceProperties
GoogleHomeProperties ServiceProperties
HomeKitProperties ServiceProperties
IFTTTProperties ServiceProperties
NestProperties ServiceProperties
SonosProperties ServiceProperties
}
// Services gets the list of 3rd-party services this controller can interface
// with.
func (c *Client) Services() ([]ServiceDefinition, error) {
body, err := c.Get("/service")
if err != nil {
return []ServiceDefinition{}, err
}
var res MultipleServiceDefinition
err = mapstructure.Decode(body, &res)
if err != nil {
return []ServiceDefinition{}, err
}
return res.Services, nil
}
type AreaDefinition struct {
Href string `json:"href"`
Name string
Category struct {
Type string
}
AssociatedDevices []HrefObject
AssociatedOccupancyGroups []HrefObject
DaylightingGainSettings HrefObject
LoadShedding HrefObject
OccupancySettings HrefObject
OccupancySensorSettings HrefObject
Parent HrefObject
}
type MultipleAreaDefinition struct {
Areas []AreaDefinition
}
type OneAreaDefinition struct {
Area AreaDefinition
}
// Areas gets the list of areas defined on this controller.
func (c *Client) Areas() ([]AreaDefinition, error) {
body, err := c.Get("/area")
if err != nil {
return []AreaDefinition{}, err
}
var res MultipleAreaDefinition
err = mapstructure.Decode(body, &res)
if err != nil {
return []AreaDefinition{}, err
}
return res.Areas, nil
}
// Area gets information about the specified area.
func (c *Client) Area(id string) (AreaDefinition, error) {
body, err := c.Get(fmt.Sprintf("/area/%s", id))
if err != nil {
return AreaDefinition{}, err
}
var res OneAreaDefinition
err = mapstructure.Decode(body, &res)
if err != nil {
return AreaDefinition{}, err
}
return res.Area, nil
}
type ZoneDefinition struct {
Name string
Href string `json:"href"`
ControlType string
Category struct {
IsLight bool
Type string
}
Device HrefObject
}
type MultipleZoneDefinition struct {
Zones []ZoneDefinition
}
type OneZoneDefinition struct {
Zone ZoneDefinition
}
// Zones gets the list of zones defined on this controller.
func (c *Client) Zones() ([]ZoneDefinition, error) {
body, err := c.Get("/zone")
if err != nil {
return []ZoneDefinition{}, err
}
var res MultipleZoneDefinition
err = mapstructure.Decode(body, &res)
if err != nil {
return []ZoneDefinition{}, err
}
return res.Zones, nil
}
// Zone gets information about the specified zone.
func (c *Client) Zone(id string) (ZoneDefinition, error) {
body, err := c.Get(fmt.Sprintf("/zone/%s", id))
if err != nil {
return ZoneDefinition{}, err
}
var res OneZoneDefinition
err = mapstructure.Decode(body, &res)
if err != nil {
return ZoneDefinition{}, err
}
return res.Zone, nil
}
type CommandParameter struct {
Type string
Value int
}
type GoToLevelCommand struct {
CommandType string
Parameter []CommandParameter
}
type GoToLevelCommandBody struct {
Command GoToLevelCommand
}
type DimmedLevelParameters struct {
DelayTime string `json:",omitempty"`
FadeTime string `json:",omitempty"`
Level int
}
type GoToDimmedLevelCommand struct {
CommandType string
DimmedLevelParameters DimmedLevelParameters
}
type GoToDimmedLevelCommandBody struct {
Command GoToDimmedLevelCommand
}
type DimOptions struct {
Delay string
Duration string
Level int
}
func (c *Client) zoneGoToLevel(id string, options DimOptions) (ZoneDefinition, error) {
body := GoToLevelCommandBody{
Command: GoToLevelCommand{
CommandType: "GoToLevel",
Parameter: []CommandParameter{
{
Type: "Level",
Value: options.Level,
},
},
},
}
raw, err := c.Post(fmt.Sprintf("/zone/%s/commandprocessor", id), body)
if err != nil {
return ZoneDefinition{}, err
}
var res OneZoneDefinition
err = mapstructure.Decode(raw, &res)
if err != nil {
return ZoneDefinition{}, err
}
return res.Zone, nil
}
func (c *Client) zoneGoToDimmedLevel(id string, options DimOptions) (ZoneDefinition, error) {
body := GoToDimmedLevelCommandBody{
Command: GoToDimmedLevelCommand{
CommandType: "GoToDimmedLevel",
DimmedLevelParameters: DimmedLevelParameters{
DelayTime: options.Delay,
FadeTime: options.Duration,
Level: options.Level,
},
},
}
raw, err := c.Post(fmt.Sprintf("/zone/%s/commandprocessor", id), body)
if err != nil {
return ZoneDefinition{}, err
}
var res OneZoneDefinition
err = mapstructure.Decode(raw, &res)
if err != nil {
return ZoneDefinition{}, err
}
return res.Zone, nil
}
// ZoneDim dims the zone to the provided level.
func (c *Client) ZoneDim(id string, options DimOptions) (ZoneDefinition, error) {
if options.Delay == "" && options.Duration == "" {
return c.zoneGoToLevel(id, options)
} else {
return c.zoneGoToDimmedLevel(id, options)
}
}
type ZoneStatus struct {
Href string `json:"href"`
Zone HrefObject
Level int
StatusAccuracy string
}
type OneZoneStatus struct {
ZoneStatus ZoneStatus
}
// ZoneStatus gets the current status of the zone.
func (c *Client) ZoneStatus(id string) (ZoneStatus, error) {
raw, err := c.Get(fmt.Sprintf("/zone/%s/status", id))
if err != nil {
return ZoneStatus{}, err
}
var res OneZoneStatus
err = mapstructure.Decode(raw, &res)
if err != nil {
return ZoneStatus{}, err
}
return res.ZoneStatus, nil
}