|
|
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) }
|