Skip to content
Merged
26 changes: 6 additions & 20 deletions bridge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"

"github.com/jarcoal/httpmock"
"github.com/stretchr/testify/assert"
)

func ExampleBridge_CreateUser() {
Expand Down Expand Up @@ -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)
}
105 changes: 96 additions & 9 deletions group.go
Original file line number Diff line number Diff line change
@@ -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
}

Expand All @@ -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)
Expand Down Expand Up @@ -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
}
116 changes: 115 additions & 1 deletion group_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package huego

import (
"github.com/stretchr/testify/assert"
"testing"

"github.com/stretchr/testify/assert"
)

func TestGetGroups(t *testing.T) {
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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)
}
}
62 changes: 61 additions & 1 deletion huego_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down