package hue import ( "context" "encoding/json" "errors" "fmt" "log" "sync" "time" "git.aiterp.net/lucifer/lucifer/internal/httperr" "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 } hl := hueLight // `hueLight` will change while the gorouting below still needs it. 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) PollButton(ctx context.Context, bridge models.Bridge, button models.Button) (<-chan models.ButtonEvent, error) { hueBridge, err := d.getBridge(bridge) if err != nil { return nil, err } channel := make(chan models.ButtonEvent, 60) go func() { fastTicker := time.NewTicker(time.Second / 30) slowTicker := time.NewTicker(time.Second / 3) ticker := slowTicker checkTicker := time.NewTicker(time.Second * 5) gotPress := make([]bool, button.NumButtons+1) lastEventTime := time.Now() lastEvent := uint16(0) lastButton := 0 for { select { case <-ticker.C: { sensor, err := hueBridge.GetSensorByIndex(button.InternalIndex) if err != nil { log.Println("Sensor poll error:", err) continue } if sensor.State.LastUpdated.Time == nil || sensor.State.LastUpdated.Before(lastEventTime) { continue } if sensor.State.LastUpdated.Equal(lastEventTime) && lastEvent == sensor.State.ButtonEvent { continue } if ticker != fastTicker { ticker = fastTicker } buttonIndex := int(sensor.State.ButtonEvent) / 1000 buttonEvent := int(sensor.State.ButtonEvent) % 1000 // Slip in a press event if there's a release not associated with a press if buttonEvent >= 2 { if !gotPress[buttonIndex] { channel <- models.ButtonEvent{Index: buttonIndex, Kind: models.ButtonEventKindPress} } } // Slip in a release event if the last button was pressed but the release got lost betwen polls if lastButton != 0 && buttonIndex != lastButton && gotPress[lastButton] { channel <- models.ButtonEvent{Index: lastButton, Kind: models.ButtonEventKindRelease} } lastEvent = sensor.State.ButtonEvent lastEventTime = *sensor.State.LastUpdated.Time lastButton = buttonIndex switch buttonEvent { case 0: // Slip in a release event if this was a consecutive press but the release got lost betwen polls if lastButton == buttonIndex && gotPress[lastButton] { channel <- models.ButtonEvent{Index: lastButton, Kind: models.ButtonEventKindRelease} } gotPress[buttonIndex] = true channel <- models.ButtonEvent{Index: buttonIndex, Kind: models.ButtonEventKindPress} case 1: gotPress[buttonIndex] = true channel <- models.ButtonEvent{Index: buttonIndex, Kind: models.ButtonEventKindRepeat} case 2, 3: gotPress[buttonIndex] = false channel <- models.ButtonEvent{Index: buttonIndex, Kind: models.ButtonEventKindRelease} } } case <-checkTicker.C: { if ticker != slowTicker && time.Since(lastEventTime) > time.Second*3 { ticker = slowTicker } } case <-ctx.Done(): { ticker.Stop() close(channel) return } } } }() return channel, nil } func (d *driver) Buttons(ctx context.Context, bridge models.Bridge) ([]models.Button, error) { hueBridge, err := d.getBridge(bridge) if err != nil { return nil, err } sensors, err := hueBridge.GetAllSensors() if err != nil { return nil, err } buttons := make([]models.Button, 0, len(sensors)) for _, sensor := range sensors { if sensor.Type == "ZLLSwitch" { buttons = append(buttons, models.Button{ ID: -1, BridgeID: bridge.ID, InternalIndex: sensor.Index, InternalID: sensor.UniqueID, Name: sensor.Name, Kind: sensor.Type, NumButtons: 4, TargetGroupID: -1, }) } } return buttons, nil } 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) { hueBridges, err := gohue.FindBridges() if err != nil { if err.Error() == "no bridges found" { // It's not my fault the library doesn't have good errors. D:< return []models.Bridge{}, nil } return nil, err } bridges := make([]models.Bridge, 0, len(hueBridges)) for _, hueBridge := range hueBridges { bridges = append(bridges, models.Bridge{ ID: -1, InternalID: hueBridge.Info.Device.SerialNumber, Driver: "hue", Name: fmt.Sprintf("New bridge (%s, %s)", hueBridge.Info.Device.FriendlyName, hueBridge.Info.Device.SerialNumber), Addr: hueBridge.IPAddress, }) } return bridges, err } 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) ForgetLight(ctx context.Context, bridge models.Bridge, light models.Light) error { hueBridge, err := d.getBridge(bridge) if err != nil { return err } hueLights, err := hueBridge.GetAllLights() if err != nil { return err } for _, hueLight := range hueLights { if light.InternalID == hueLight.UniqueID { return hueLight.Delete() } } return httperr.NotFound("Light") } 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) }