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.
306 lines
6.4 KiB
306 lines
6.4 KiB
package hue
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.aiterp.net/lucifer/lucifer/internal/huecolor"
|
|
|
|
"git.aiterp.net/lucifer/lucifer/light"
|
|
"git.aiterp.net/lucifer/lucifer/models"
|
|
gohue "github.com/collinux/gohue"
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
const (
|
|
// FlagUseXY applies a more aggressive mode change via xy to make TradFri bulbs work.
|
|
FlagUseXY = 1
|
|
)
|
|
|
|
type xyBri struct {
|
|
XY [2]float32
|
|
Bri uint8
|
|
}
|
|
|
|
func colorKey(light models.Light) string {
|
|
return fmt.Sprintf("%s.%s.%d", light.InternalID, light.Color, light.Brightness)
|
|
}
|
|
|
|
// A driver is a driver for Phillips Hue lights.
|
|
type driver struct {
|
|
mutex sync.Mutex
|
|
bridges map[int]*gohue.Bridge
|
|
colors map[string]xyBri
|
|
}
|
|
|
|
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, _ := 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 {
|
|
if !light.On {
|
|
return hl.SetState(gohue.LightState{
|
|
On: false,
|
|
})
|
|
}
|
|
|
|
x, y, bri, err := d.calcColor(light, hl)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Printf("Updating light (id: %d, rgb: %s, xy: [%f, %f], bri: %d)", light.ID, light.Color, x, y, bri)
|
|
|
|
err = hl.SetState(gohue.LightState{
|
|
On: light.On,
|
|
XY: &[2]float32{float32(x), float32(y)},
|
|
Bri: bri,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
hl2, err := hueBridge.GetLightByIndex(hl.Index)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
d.mutex.Lock()
|
|
d.colors[colorKey(light)] = xyBri{XY: hl2.State.XY, Bri: hl2.State.Bri}
|
|
d.mutex.Unlock()
|
|
|
|
return nil
|
|
})
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
return eg.Wait()
|
|
}
|
|
|
|
func (d *driver) DiscoverLights(ctx context.Context, bridge models.Bridge) error {
|
|
hueBridge, err := d.getBridge(bridge)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return hueBridge.FindNewLights()
|
|
}
|
|
|
|
func (d *driver) Lights(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 {
|
|
r, g, b := huecolor.ConvertRGB(float64(hueLight.State.XY[0]), float64(hueLight.State.XY[1]), float64(hueLight.State.Bri)/255)
|
|
|
|
light := models.Light{
|
|
ID: -1,
|
|
Name: hueLight.Name,
|
|
BridgeID: bridge.ID,
|
|
InternalID: hueLight.UniqueID,
|
|
On: hueLight.State.On,
|
|
}
|
|
light.SetColorRGBf(r, g, b)
|
|
light.Brightness = hueLight.State.Bri
|
|
|
|
lights = append(lights, light)
|
|
}
|
|
|
|
return lights, nil
|
|
}
|
|
|
|
func (d *driver) Bridges(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 {
|
|
return models.Bridge{}, 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("Bridge discovery timed out after 30 failed attempts")
|
|
}
|
|
|
|
func (d *driver) ChangedLights(ctx context.Context, bridge models.Bridge, lights ...models.Light) ([]models.Light, error) {
|
|
hueBridge, err := d.getBridge(bridge)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
hueLights, err := hueBridge.GetAllLights()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
subset := make([]models.Light, 0, len(lights))
|
|
|
|
for _, hueLight := range hueLights {
|
|
for _, light := range lights {
|
|
if hueLight.UniqueID != light.InternalID {
|
|
continue
|
|
}
|
|
|
|
d.mutex.Lock()
|
|
c, cOk := d.colors[colorKey(light)]
|
|
d.mutex.Unlock()
|
|
|
|
if !cOk || c.Bri != hueLight.State.Bri || diff(c.XY[0], hueLight.State.XY[0]) > 0.064 || diff(c.XY[1], hueLight.State.XY[1]) > 0.064 {
|
|
subset = append(subset, light)
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
return subset, nil
|
|
}
|
|
|
|
func (d *driver) calcColor(light models.Light, hueLight gohue.Light) (x, y float64, bri uint8, err error) {
|
|
r, g, b, err := light.ColorRGBf()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
x, y = huecolor.ConvertXY(r, g, b)
|
|
bri = light.Brightness
|
|
if bri < 1 {
|
|
bri = 1
|
|
} else if bri > 254 {
|
|
bri = 254
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (d *driver) getRawState(hueLight gohue.Light) (map[string]interface{}, error) {
|
|
data := struct {
|
|
State map[string]interface{} `json:"state"`
|
|
}{
|
|
State: make(map[string]interface{}, 16),
|
|
}
|
|
|
|
uri := fmt.Sprintf("/api/%s/lights/%d/", hueLight.Bridge.Username, hueLight.Index)
|
|
_, reader, err := hueLight.Bridge.Get(uri)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = json.NewDecoder(reader).Decode(&data)
|
|
return data.State, err
|
|
}
|
|
|
|
func (d *driver) setState(hueLight gohue.Light, key string, value interface{}) error {
|
|
m := make(map[string]interface{}, 1)
|
|
m[key] = value
|
|
|
|
uri := fmt.Sprintf("/api/%s/lights/%d/state", hueLight.Bridge.Username, hueLight.Index)
|
|
_, _, err := hueLight.Bridge.Put(uri, m)
|
|
|
|
return err
|
|
}
|
|
|
|
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 diff(a, b float32) float32 {
|
|
diff := a - b
|
|
if diff < 0 {
|
|
return -diff
|
|
}
|
|
|
|
return diff
|
|
}
|
|
|
|
func init() {
|
|
driver := &driver{
|
|
bridges: make(map[int]*gohue.Bridge, 16),
|
|
colors: make(map[string]xyBri, 128),
|
|
}
|
|
|
|
light.RegisterDriver("hue", driver)
|
|
}
|