The main server, and probably only repository in this org.
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

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