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