diff --git a/bridge_test.go b/bridge_test.go index 8bd9677..61a7edf 100644 --- a/bridge_test.go +++ b/bridge_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" ) func ExampleBridge_CreateUser() { @@ -65,49 +66,34 @@ func TestUpdateBridgeConfigError(t *testing.T) { func TestBridge_getAPIPathError(t *testing.T) { b := New("invalid hostname", "") - expected := "parse http://invalid hostname: invalid character \" \" in host name" _, err := b.getAPIPath("/") - if err.Error() != expected { - t.Fatalf("Expected error %s but got %s", expected, err.Error()) - } + assert.NotNil(t, err) } func TestBridge_getError(t *testing.T) { httpmock.Deactivate() defer httpmock.Activate() - expected := "Get invalid%20hostname: unsupported protocol scheme \"\"" _, err := get(context.Background(), "invalid hostname") - if err.Error() != expected { - t.Fatalf("Expected error %s but got %s", expected, err.Error()) - } + assert.NotNil(t, err) } func TestBridge_putError(t *testing.T) { httpmock.Deactivate() defer httpmock.Activate() - expected := "Put invalid%20hostname: unsupported protocol scheme \"\"" _, err := put(context.Background(), "invalid hostname", []byte("huego")) - if err.Error() != expected { - t.Fatalf("Expected error %s but got %s", expected, err.Error()) - } + assert.NotNil(t, err) } func TestBridge_postError(t *testing.T) { httpmock.Deactivate() defer httpmock.Activate() - expected := "Post invalid%20hostname: unsupported protocol scheme \"\"" _, err := post(context.Background(), "invalid hostname", []byte("huego")) - if err.Error() != expected { - t.Fatalf("Expected error %s but got %s", expected, err.Error()) - } + assert.NotNil(t, err) } func TestBridge_deleteError(t *testing.T) { httpmock.Deactivate() defer httpmock.Activate() - expected := "Delete invalid%20hostname: unsupported protocol scheme \"\"" _, err := delete(context.Background(), "invalid hostname") - if err.Error() != expected { - t.Fatalf("Expected error %s but got %s", expected, err.Error()) - } + assert.NotNil(t, err) } diff --git a/group.go b/group.go index fd52a49..a6b77dd 100644 --- a/group.go +++ b/group.go @@ -1,17 +1,22 @@ package huego -import "context" +import ( + "context" + "errors" +) // Group represents a bridge group https://developers.meethue.com/documentation/groups-api type Group struct { - Name string `json:"name,omitempty"` - Lights []string `json:"lights,omitempty"` - Type string `json:"type,omitempty"` - GroupState *GroupState `json:"state,omitempty"` - Recycle bool `json:"recycle,omitempty"` - Class string `json:"class,omitempty"` - State *State `json:"action,omitempty"` - ID int `json:"-"` + Name string `json:"name,omitempty"` + Lights []string `json:"lights,omitempty"` + Type string `json:"type,omitempty"` + GroupState *GroupState `json:"state,omitempty"` + Recycle bool `json:"recycle,omitempty"` + Class string `json:"class,omitempty"` + Stream *Stream `json:"stream,omitempty"` + Locations map[string][]float64 `json:"locations,omitempty"` + State *State `json:"action,omitempty"` + ID int `json:"-"` bridge *Bridge } @@ -22,6 +27,32 @@ type GroupState struct { AnyOn bool `json:"any_on,omitempty"` } +// Stream define the stream status of a group +type Stream struct { + ProxyMode string `json:"proxymode,omitempty"` + ProxyNode string `json:"proxynode,omitempty"` + ActiveRaw *bool `json:"active,omitempty"` + OwnerRaw *string `json:"owner,omitempty"` +} + +// Active returns the stream active state, and will return false if ActiveRaw is nil +func (s *Stream) Active() bool { + if s.ActiveRaw == nil { + return false + } + + return *s.ActiveRaw +} + +// Owner returns the stream Owner, and will return an empty string if OwnerRaw is nil +func (s *Stream) Owner() string { + if s.OwnerRaw == nil { + return "" + } + + return *s.OwnerRaw +} + // SetState sets the state of the group to s. func (g *Group) SetState(s State) error { return g.SetStateContext(context.Background(), s) @@ -239,3 +270,59 @@ func (g *Group) AlertContext(ctx context.Context, new string) error { g.State.Effect = new return nil } + +// EnableStreaming enables streaming for the group by setting the Stream Active property to true +func (g *Group) EnableStreaming() error { + return g.EnableStreamingContext(context.Background()) +} + +// EnableStreamingContext enables streaming for the group by setting the Stream Active property to true +func (g *Group) EnableStreamingContext(ctx context.Context) error { + if g.Type != "Entertainment" { + return errors.New("must be an entertainment group to enable streaming") + } + + active := true + update := Group{ + Stream: &Stream{ + ActiveRaw: &active, + }, + } + _, err := g.bridge.UpdateGroupContext(ctx, g.ID, update) + if err != nil { + return err + } + + g.Stream.ActiveRaw = &active + g.Stream.OwnerRaw = &g.bridge.User + + return nil +} + +// DisableStreaming disabled streaming for the group by setting the Stream Active property to false +func (g *Group) DisableStreaming() error { + return g.DisableStreamingContext(context.Background()) +} + +// DisableStreamingContext disabled streaming for the group by setting the Stream Active property to false +func (g *Group) DisableStreamingContext(ctx context.Context) error { + if g.Type != "Entertainment" { + return errors.New("must be an entertainment group to disable streaming") + } + + active := false + update := Group{ + Stream: &Stream{ + ActiveRaw: &active, + }, + } + _, err := g.bridge.UpdateGroupContext(ctx, g.ID, update) + if err != nil { + return err + } + + g.Stream.ActiveRaw = &active + g.Stream.OwnerRaw = nil + + return nil +} diff --git a/group_test.go b/group_test.go index 34a3fe0..98a133e 100644 --- a/group_test.go +++ b/group_test.go @@ -1,8 +1,9 @@ package huego import ( - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestGetGroups(t *testing.T) { @@ -97,6 +98,45 @@ func TestGetGroup(t *testing.T) { assert.NotNil(t, err) } +func TestGetEntertainmentGroup(t *testing.T) { + b := New(hostname, username) + g, err := b.GetGroup(3) + if err != nil { + t.Fatal(err) + } + if g.Stream.Active() { + t.Fatal("group stream should be inactive") + } + if owner := g.Stream.Owner(); owner != "" { + t.Fatalf("group stream should have no owner. got: %s", owner) + } + + b = New(hostname, username) + g, err = b.GetGroup(4) + if err != nil { + t.Fatal(err) + } + if !g.Stream.Active() { + t.Fatal("group stream should be active") + } + if want, owner := "QZTPWY1ADZDM8IG188LBVOB5YV5O5OPZNCKTQPQB", g.Stream.Owner(); owner != want { + t.Fatalf("group stream should have owner. got: %s, want :%s", owner, want) + } + + b = New(hostname, username) + g, err = b.GetGroup(2) + if err != nil { + t.Fatal(err) + } + g.Stream = &Stream{} + if g.Stream.Active() { + t.Fatal("group stream should be inactive") + } + if owner := g.Stream.Owner(); owner != "" { + t.Fatalf("group stream should have no owner. got: %s", owner) + } +} + func TestCreateGroup(t *testing.T) { b := New(hostname, username) group := Group{ @@ -432,3 +472,77 @@ func TestDeleteGroup(t *testing.T) { t.Logf("Deleted group with id: %d", id) } } + +func TestEnableStreamingGroup(t *testing.T) { + bridge := New(hostname, username) + id := 3 + group, err := bridge.GetGroup(id) + if err != nil { + t.Fatal(err) + } + err = group.EnableStreaming() + if err != nil { + t.Fatal(err) + } + + id = 2 + group, err = bridge.GetGroup(id) + if err != nil { + t.Fatal(err) + } + err = group.EnableStreaming() + if err == nil { + t.Fatal("error was nil") + } else if errString := err.Error(); errString != "must be an entertainment group to enable streaming" { + t.Fatalf("incorrect error: %s", errString) + } + + id = 5 + group, err = bridge.GetGroup(id) + if err != nil { + t.Fatal(err) + } + err = group.EnableStreaming() + if err == nil { + t.Fatal("error was nil") + } else if errString := err.Error(); errString != "ERROR 307 [/groups/5/stream/active]: \"Cannot claim stream ownership\"" { + t.Fatalf("incorrect error: %s", errString) + } +} + +func TestDisableStreamingGroup(t *testing.T) { + bridge := New(hostname, username) + id := 3 + group, err := bridge.GetGroup(id) + if err != nil { + t.Fatal(err) + } + err = group.DisableStreaming() + if err != nil { + t.Fatal(err) + } + + id = 2 + group, err = bridge.GetGroup(id) + if err != nil { + t.Fatal(err) + } + err = group.DisableStreaming() + if err == nil { + t.Fatal("error was nil") + } else if errString := err.Error(); errString != "must be an entertainment group to disable streaming" { + t.Fatalf("incorrect error %s", errString) + } + + id = 6 + group, err = bridge.GetGroup(id) + if err != nil { + t.Fatal(err) + } + err = group.DisableStreaming() + if err == nil { + t.Fatal("error was nil") + } else if errString := err.Error(); errString != "ERROR 999 [/groups/6/stream/active]: \"unspecified error\"" { + t.Fatalf("incorrect error %s", errString) + } +} diff --git a/huego_test.go b/huego_test.go index f60401b..c1188ff 100644 --- a/huego_test.go +++ b/huego_test.go @@ -109,7 +109,7 @@ func init() { { method: "GET", path: "/groups", - data: `{"1":{"name":"Group 1","lights":["1","2"],"type":"LightGroup","state":{"all_on":true,"any_on":true},"action":{"on":true,"bri":254,"hue":10000,"sat":254,"effect":"none","xy":[0.5,0.5],"ct":250,"alert":"select","colormode":"ct"}},"2":{"name":"Group 2","lights":["3","4","5"],"type":"LightGroup","state":{"all_on":true,"any_on":true},"action":{"on":true,"bri":153,"hue":4345,"sat":254,"effect":"none","xy":[0.5,0.5],"ct":250,"alert":"select","colormode":"ct"}}}`, + data: `{"1":{"name":"Group 1","lights":["1","2"],"type":"LightGroup","state":{"all_on":true,"any_on":true},"action":{"on":true,"bri":254,"hue":10000,"sat":254,"effect":"none","xy":[0.5,0.5],"ct":250,"alert":"select","colormode":"ct"}},"2":{"name":"Group 2","lights":["3","4","5"],"type":"LightGroup","state":{"all_on":true,"any_on":true},"action":{"on":true,"bri":153,"hue":4345,"sat":254,"effect":"none","xy":[0.5,0.5],"ct":250,"alert":"select","colormode":"ct"}},"3":{"name":"Group 3","lights":["1","2","3","4","5"],"sensors":[],"type":"Entertainment","state":{"all_on":true,"any_on":true},"recycle":false,"class":"TV","stream":{"proxymode":"auto","proxynode":"/lights/3","active":false,"owner":null},"locations":{"1":[0.93,-0.92,0.00],"2":[0.13,-0.85,1.00],"3":[-0.03,-0.86,1.00],"4":[-0.40,1.00,0.00],"5":[0.43,1.00,0.00]},"action":{"on":true,"bri":62,"hue":43749,"sat":189,"effect":"none","xy":[0.2133,0.2075],"ct":153,"alert":"select","colormode":"xy"}}}`, }, { method: "GET", @@ -137,6 +137,66 @@ func init() { data: `[{"success":"/groups/1 deleted."}]`, }, + // NON-ENTERTAINMENT GROUP + { + method: "GET", + path: "/groups/2", + data: `{"name":"Office","lights":["4","5","1","2","3"],"sensors":[],"type":"Room","state":{"all_on":true,"any_on":true},"recycle":false,"class":"Office","action":{"on":true,"bri":92,"hue":53702,"sat":82,"effect":"none","xy":[ 0.3693, 0.3006],"ct":233,"alert":"select","colormode":"xy"}}`, + }, + { + method: "PUT", + path: "/groups/2", + data: `[{"error":{"type":6,"address":"/groups/2/stream","description":"parameter, /groups/2/stream, not available"}}]`, + }, + + // INACTIVE ENTERTAINMENT GROUP + { + method: "GET", + path: "/groups/3", + data: `{"name":"Group 3","lights":["1","2","3","4","5"],"sensors":[],"type":"Entertainment","state":{"all_on":true,"any_on":true},"recycle":false,"class":"TV","stream":{"proxymode":"auto","proxynode":"/lights/3","active":false,"owner":null},"locations":{"1":[0.93,-0.92,0.00],"2":[0.13,-0.85,1.00],"3":[-0.03,-0.86,1.00],"4":[-0.40,1.00,0.00],"5":[0.43,1.00,0.00]},"action":{"on":true,"bri":62,"hue":43749,"sat":189,"effect":"none","xy":[0.2133,0.2075],"ct":153,"alert":"select","colormode":"xy"}}`, + }, + { + method: "PUT", + path: "/groups/3", + data: `[{"success":{"/groups/3/stream/active":true}}]`, + }, + + // ACTIVE ENTERTAINMENT GROUP + { + method: "GET", + path: "/groups/4", + data: `{"name":"Group 4","lights":["1","2","3","4","5"],"sensors":[],"type":"Entertainment","state":{"all_on":true,"any_on":true},"recycle":false,"class":"TV","stream":{"proxymode":"auto","proxynode":"/lights/3","active":true,"owner":"QZTPWY1ADZDM8IG188LBVOB5YV5O5OPZNCKTQPQB"},"locations":{"1":[0.93,-0.92,0.00],"2":[0.13,-0.85,1.00],"3":[-0.03,-0.86,1.00],"4":[-0.40,1.00,0.00],"5":[0.43,1.00,0.00]},"action":{"on":true,"bri":62,"hue":43749,"sat":189,"effect":"none","xy":[0.2133,0.2075],"ct":153,"alert":"select","colormode":"xy"}}`, + }, + { + method: "PUT", + path: "/groups/4", + data: `[{"success":{"/groups/3/stream/active":false}}]`, + }, + + // ACTIVE ENTERTAINMENT GROUP FOR ENABLE ERROR + { + method: "GET", + path: "/groups/5", + data: `{"name":"Group 5","lights":["1","2","3","4","5"],"sensors":[],"type":"Entertainment","state":{"all_on":true,"any_on":true},"recycle":false,"class":"TV","stream":{"proxymode":"auto","proxynode":"/lights/3","active":true,"owner":"QZTPWY1ADZDM8IG188LBVOB5YV5O5OPZNCKTQPQB"},"locations":{"1":[0.93,-0.92,0.00],"2":[0.13,-0.85,1.00],"3":[-0.03,-0.86,1.00],"4":[-0.40,1.00,0.00],"5":[0.43,1.00,0.00]},"action":{"on":true,"bri":62,"hue":43749,"sat":189,"effect":"none","xy":[0.2133,0.2075],"ct":153,"alert":"select","colormode":"xy"}}`, + }, + { + method: "PUT", + path: "/groups/5", + data: `[{"error":{"type":307,"address":"/groups/5/stream/active","description":"Cannot claim stream ownership"}}]`, + }, + + // ACTIVE ENTERTAINMENT GROUP FOR DISABLE ERROR + { + method: "GET", + path: "/groups/6", + data: `{"name":"Group 6","lights":["1","2","3","4","5"],"sensors":[],"type":"Entertainment","state":{"all_on":true,"any_on":true},"recycle":false,"class":"TV","stream":{"proxymode":"auto","proxynode":"/lights/3","active":true,"owner":"QZTPWY1ADZDM8IG188LBVOB5YV5O5OPZNCKTQPQB"},"locations":{"1":[0.93,-0.92,0.00],"2":[0.13,-0.85,1.00],"3":[-0.03,-0.86,1.00],"4":[-0.40,1.00,0.00],"5":[0.43,1.00,0.00]},"action":{"on":true,"bri":62,"hue":43749,"sat":189,"effect":"none","xy":[0.2133,0.2075],"ct":153,"alert":"select","colormode":"xy"}}`, + }, + { + method: "PUT", + path: "/groups/6", + data: `[{"error":{"type":999,"address":"/groups/6/stream/active","description":"unspecified error"}}]`, + }, + // SCENE { method: "GET",