You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
169 lines
3.3 KiB
169 lines
3.3 KiB
package hue
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.aiterp.net/lucifer/lucifer/light"
|
|
"git.aiterp.net/lucifer/lucifer/models"
|
|
gohue "github.com/collinux/gohue"
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
// A driver is a driver for Phillips Hue lights.
|
|
type driver struct {
|
|
mutex sync.Mutex
|
|
bridges map[int]*gohue.Bridge
|
|
}
|
|
|
|
func (d *driver) Apply(ctx context.Context, bridge models.Bridge, lights ...models.Light) error {
|
|
hueBridge, err := d.getBridge(bridge)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
hueLights, err := hueBridge.GetAllLights()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
eg, egCtx := errgroup.WithContext(ctx)
|
|
|
|
for _, hueLight := range hueLights {
|
|
if !hueLight.State.Reachable {
|
|
continue
|
|
}
|
|
|
|
for _, light := range lights {
|
|
if hueLight.UniqueID != light.InternalID {
|
|
continue
|
|
}
|
|
|
|
// Prevent race condition since `hueLight` changes per iteration.
|
|
hl := hueLight
|
|
|
|
eg.Go(func() error {
|
|
select {
|
|
case <-egCtx.Done():
|
|
return egCtx.Err()
|
|
default:
|
|
}
|
|
|
|
return hl.SetState(gohue.LightState{
|
|
On: light.On,
|
|
Hue: light.Color.Hue,
|
|
Sat: light.Color.Sat,
|
|
Bri: light.Color.Bri,
|
|
})
|
|
})
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
return eg.Wait()
|
|
}
|
|
|
|
func (d *driver) DiscoverLights(ctx context.Context, bridge models.Bridge) ([]models.Light, error) {
|
|
hueBridge, err := d.getBridge(bridge)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
hueLights, err := hueBridge.GetAllLights()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
lights := make([]models.Light, 0, len(hueLights))
|
|
for _, hueLight := range hueLights {
|
|
lights = append(lights, models.Light{
|
|
ID: -1,
|
|
Name: hueLight.Name,
|
|
BridgeID: bridge.ID,
|
|
InternalID: hueLight.UniqueID,
|
|
On: hueLight.State.On,
|
|
Color: models.LightColor{
|
|
Hue: hueLight.State.Hue,
|
|
Sat: hueLight.State.Saturation,
|
|
Bri: hueLight.State.Bri,
|
|
},
|
|
})
|
|
}
|
|
|
|
return lights, nil
|
|
}
|
|
|
|
func (d *driver) DiscoverBridges(ctx context.Context) ([]models.Bridge, error) {
|
|
panic("not implemented")
|
|
}
|
|
|
|
func (d *driver) Connect(ctx context.Context, bridge models.Bridge) (models.Bridge, error) {
|
|
hueBridge, err := gohue.NewBridge(bridge.Addr)
|
|
if err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
|
|
// Make 30 attempts (30 seconds)
|
|
attempts := 30
|
|
for attempts > 0 {
|
|
key, err := hueBridge.CreateUser("Lucifer (git.aiterp.net/lucifer/lucifer)")
|
|
if len(key) > 0 && err == nil {
|
|
bridge.Key = []byte(key)
|
|
bridge.InternalID = hueBridge.Info.Device.SerialNumber
|
|
|
|
return bridge, nil
|
|
}
|
|
|
|
select {
|
|
case <-time.After(time.Second):
|
|
attempts--
|
|
case <-ctx.Done():
|
|
return models.Bridge{}, ctx.Err()
|
|
}
|
|
}
|
|
|
|
return models.Bridge{}, errors.New("Failed to create bridge")
|
|
}
|
|
|
|
func (d *driver) getBridge(bridge models.Bridge) (*gohue.Bridge, error) {
|
|
d.mutex.Lock()
|
|
defer d.mutex.Unlock()
|
|
|
|
if hueBridge, ok := d.bridges[bridge.ID]; ok {
|
|
return hueBridge, nil
|
|
}
|
|
|
|
hueBridge, err := gohue.NewBridge(bridge.Addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := hueBridge.GetInfo(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if hueBridge.Info.Device.SerialNumber != bridge.InternalID {
|
|
return nil, errors.New("Serial number does not match hardware")
|
|
}
|
|
|
|
err = hueBridge.Login(string(bridge.Key))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
d.bridges[bridge.ID] = hueBridge
|
|
|
|
return hueBridge, nil
|
|
}
|
|
|
|
func init() {
|
|
driver := &driver{
|
|
bridges: make(map[int]*gohue.Bridge, 16),
|
|
}
|
|
|
|
light.RegisterDriver("hue", driver)
|
|
}
|