diff --git a/cmd/xy/main.go b/cmd/xy/main.go index a164ee8..7ce6799 100644 --- a/cmd/xy/main.go +++ b/cmd/xy/main.go @@ -1,49 +1,21 @@ package main import ( - "context" "encoding/json" - "fmt" - "git.aiterp.net/lucifer/new-server/internal/drivers/hue2" "git.aiterp.net/lucifer/new-server/models" "log" ) func main() { - client := hue2.NewClient("10.80.8.8", "o2XKGgmVUGNBghYFdLUCVuinOTMxFH4pHV9PuTbU") - bridge := hue2.NewBridge(client) - - err := bridge.RefreshAll(context.Background()) - if err != nil { - log.Fatalln(err) - } - j, _ := json.Marshal(bridge.GenerateDevices()) - fmt.Println(string(j)) - - ch := make(chan models.Event) - go func() { - for event := range ch { - log.Println("EVENT", event.Name, event.Payload) - } - }() - - for i, dev := range bridge.GenerateDevices() { - device := dev - switch device.InternalID { - case "6d5a45b0-ec69-4417-8588-717358b05086": - c, _ := models.ParseColorValue("xy:0.22,0.18") - device.State.Color = c - device.State.Intensity = 0.3 - case "a71128f4-5295-4ae4-9fbc-5541abc8739b": - c, _ := models.ParseColorValue("k:6500") - device.State.Color = c - device.State.Intensity = 0.2 - } - - device.ID = i + 1 - bridge.Update(device) - } + cv, _ := models.ParseColorValue("rgb:#fff") + hs, _ := cv.ToHS() + log.Println(cv.String()) + log.Println(hs.String()) + xy, _ := hs.ToXY() + log.Println(xy.String()) +} - err = bridge.Run(context.Background(), ch) - log.Println(err) +func toJSON(v interface{}) string { + j, _ := json.Marshal(v) + return string(j) } diff --git a/internal/drivers/hue/state.go b/internal/drivers/hue/state.go index 3d3a46f..58aa82d 100644 --- a/internal/drivers/hue/state.go +++ b/internal/drivers/hue/state.go @@ -40,7 +40,7 @@ func (s *hueLightState) Update(state models.DeviceState) { if state.Power { input.On = ptrBool(true) if state.Color.IsKelvin() { - input.CT = ptrInt(1000000 / state.Color.Kelvin) + input.CT = ptrInt(1000000 / *state.Color.K) if *input.CT < s.info.Capabilities.Control.CT.Min { *input.CT = s.info.Capabilities.Control.CT.Min } @@ -51,13 +51,13 @@ func (s *hueLightState) Update(state models.DeviceState) { if s.input.CT == nil || *s.input.CT != *input.CT { s.stale = true } - } else { - input.Hue = ptrInt(int(state.Color.Hue*(65536/360)) % 65536) + } else if color, ok := state.Color.ToHS(); ok { + input.Hue = ptrInt(int(color.HS.Hue*(65536/360)) % 65536) if s.input.Hue == nil || *s.input.Hue != *input.Hue { s.stale = true } - input.Sat = ptrInt(int(state.Color.Saturation * 255)) + input.Sat = ptrInt(int(color.HS.Sat * 255)) if *input.Sat > 254 { *input.Sat = 254 } diff --git a/internal/drivers/hue2/bridge.go b/internal/drivers/hue2/bridge.go index 8cf425d..3466721 100644 --- a/internal/drivers/hue2/bridge.go +++ b/internal/drivers/hue2/bridge.go @@ -269,7 +269,7 @@ func (b *Bridge) MakeCongruent(ctx context.Context) (int, error) { lightsOut := light.Power != nil && !device.State.Power if !lightsOut { if light.ColorTemperature != nil && device.State.Color.IsKelvin() { - mirek := 1000000 / device.State.Color.Kelvin + mirek := 1000000 / *device.State.Color.K if mirek < light.ColorTemperature.MirekSchema.MirekMinimum { mirek = light.ColorTemperature.MirekSchema.MirekMinimum } @@ -280,9 +280,10 @@ func (b *Bridge) MakeCongruent(ctx context.Context) (int, error) { update.Mirek = &mirek changed = true } - } else if xy, ok := device.State.Color.ToXY(); ok && light.Color != nil { - xy = light.Color.Gamut.Conform(xy).Round() + } else if xyColor, ok := device.State.Color.ToXY(); ok && light.Color != nil { + xy := light.Color.Gamut.Conform(*xyColor.XY).Round() if xy.DistanceTo(light.Color.XY) > 0.0009 || (light.ColorTemperature != nil && light.ColorTemperature.Mirek != nil) { + log.Println(xyColor.String(), device.State.Color.String()) update.ColorXY = &xy changed = true } @@ -419,9 +420,7 @@ func (b *Bridge) GenerateDevices() []models.Device { } if light.ColorTemperature != nil { if light.ColorTemperature.Mirek != nil { - device.State.Color = models.ColorValue{ - Kelvin: int(1000000 / *light.ColorTemperature.Mirek), - } + device.State.Color.SetK(1000000 / *light.ColorTemperature.Mirek) } device.Capabilities = append(device.Capabilities, models.DCColorKelvin) device.DriverProperties["maxTemperature"] = 1000000 / light.ColorTemperature.MirekSchema.MirekMinimum @@ -530,9 +529,11 @@ func (b *Bridge) applyPatches(patches []ResourceData) { resCopy.Color = &cp resCopy.Color.XY = patch.Color.XY - cp2 := *resCopy.ColorTemperature - resCopy.ColorTemperature = &cp2 - resCopy.ColorTemperature.Mirek = nil + if resCopy.ColorTemperature != nil { + cp2 := *resCopy.ColorTemperature + resCopy.ColorTemperature = &cp2 + resCopy.ColorTemperature.Mirek = nil + } } if patch.ColorTemperature != nil && resCopy.ColorTemperature != nil { cp := *resCopy.ColorTemperature diff --git a/internal/drivers/lifx/bridge.go b/internal/drivers/lifx/bridge.go index 410ecd1..9222999 100644 --- a/internal/drivers/lifx/bridge.go +++ b/internal/drivers/lifx/bridge.go @@ -169,9 +169,11 @@ func (b *Bridge) Run(ctx context.Context, debug bool) error { state.deviceState = &models.DeviceState{ Power: p.On, Color: models.ColorValue{ - Hue: p.Hue, - Saturation: p.Sat, - Kelvin: p.Kelvin, + HS: &models.ColorHS{ + Hue: p.Hue, + Sat: p.Sat, + }, + K: &p.Kelvin, }, Intensity: p.Bri, } @@ -204,7 +206,7 @@ func (b *Bridge) Run(ctx context.Context, debug bool) error { b.checkAndUpdateState(state) } - if time.Since(state.discoveredTime) > time.Second*10 && time.Since(state.fwSpamTime) > time.Second * 30 { + if time.Since(state.discoveredTime) > time.Second*10 && time.Since(state.fwSpamTime) > time.Second*30 { state.fwSpamTime = time.Now() if state.firmware == nil { diff --git a/internal/drivers/lifx/state.go b/internal/drivers/lifx/state.go index bb0fac1..458ee82 100644 --- a/internal/drivers/lifx/state.go +++ b/internal/drivers/lifx/state.go @@ -36,17 +36,21 @@ func (s *State) generateUpdate() []Payload { return results } - c := s.deviceState.Color + c, ok := s.deviceState.Color.ToHSK() + if !ok { + c, _ = models.ParseColorValue("hsk:0,0,4000") + } + l := s.lightState di := s.deviceState.Intensity - k := c.Kelvin + k := *c.K if k == 0 { k = 4000 } - if !equalish(c.Hue, l.Hue) || !equalish(c.Saturation, l.Sat) || !equalish(di, l.Bri) || k != l.Kelvin { + if !equalish(c.HS.Hue, l.Hue) || !equalish(c.HS.Sat, l.Sat) || !equalish(di, l.Bri) || k != l.Kelvin { results = append(results, &SetColor{ - Hue: c.Hue, - Sat: c.Saturation, + Hue: c.HS.Hue, + Sat: c.HS.Sat, Bri: di, Kelvin: k, TransitionTime: time.Millisecond * 150, @@ -72,11 +76,16 @@ func (s *State) handleAck(seq uint8) { prevLabel = s.lightState.Label } + color, ok := s.deviceState.Color.ToHSK() + if !ok { + color, _ = models.ParseColorValue("hsk:0,0,4000") + } + s.lightState = &LightState{ - Hue: s.deviceState.Color.Hue, - Sat: s.deviceState.Color.Saturation, + Hue: color.HS.Hue, + Sat: color.HS.Sat, Bri: s.deviceState.Intensity, - Kelvin: s.deviceState.Color.Kelvin, + Kelvin: *color.K, On: s.deviceState.Power, Label: prevLabel, } diff --git a/internal/drivers/nanoleaf/bridge.go b/internal/drivers/nanoleaf/bridge.go index ce5d916..60d0fab 100644 --- a/internal/drivers/nanoleaf/bridge.go +++ b/internal/drivers/nanoleaf/bridge.go @@ -10,7 +10,6 @@ import ( "io" "io/ioutil" "log" - "math" "net" "net/http" "strconv" @@ -31,11 +30,12 @@ type bridge struct { func (b *bridge) Devices() []models.Device { results := make([]models.Device, 0, len(b.panels)) for i, panel := range b.panels { + // Find normalized RGB and intensity red := float64(panel.ColorRGBA[0]) / 255.0 green := float64(panel.ColorRGBA[1]) / 255.0 blue := float64(panel.ColorRGBA[2]) / 255.0 - - hue, sat, value := colorful.LinearRgb(red, green, blue).Hsv() + hue, sat, value := colorful.Color{R: red, G: green, B: blue}.Hsv() + rgb := colorful.Hsv(hue, sat, 1) shapeType, shapeTypeOK := shapeTypeMap[panel.ShapeType] if !shapeTypeOK { @@ -70,10 +70,11 @@ func (b *bridge) Devices() []models.Device { UserProperties: nil, State: models.DeviceState{ Power: panel.ColorRGBA[3] == 0, - Color: models.ColorValue{ - Hue: math.Mod(hue, 360), - Saturation: sat, - }, + Color: models.ColorValue{RGB: &models.ColorRGB{ + Red: rgb.R, + Green: rgb.G, + Blue: rgb.B, + }}, Intensity: value, Temperature: 0, }, @@ -172,9 +173,20 @@ func (b *bridge) Update(devices []models.Device) { for _, panel := range b.panels { if panel.ID == uint16(id) { if device.State.Power { - color := colorful.Hsv(device.State.Color.Hue, device.State.Color.Saturation, device.State.Intensity) + rgbColor, ok := device.State.Color.ToRGB() + if !ok { + newColor := [4]byte{255, 255, 255, 255} + if newColor != panel.ColorRGBA { + panel.update(newColor, time.Now().Add(time.Millisecond*220)) + } + + continue + } - red, green, blue := color.RGB255() + rgb := rgbColor.RGB.AtIntensity(device.State.Intensity) + red := byte(rgb.Red * 255.9) + green := byte(rgb.Green * 255.9) + blue := byte(rgb.Blue * 255.9) newColor := [4]byte{red, green, blue, 255} if newColor != panel.ColorRGBA { panel.update(newColor, time.Now().Add(time.Millisecond*220)) diff --git a/internal/mysql/devicerepo.go b/internal/mysql/devicerepo.go index f077ffb..db1a397 100644 --- a/internal/mysql/devicerepo.go +++ b/internal/mysql/devicerepo.go @@ -30,6 +30,7 @@ type deviceStateRecord struct { Power bool `db:"power"` Intensity float64 `db:"intensity"` Temperature int `db:"temperature"` + Color string `db:"color"` } type devicePropertyRecord struct { @@ -228,13 +229,14 @@ func (r *DeviceRepo) SaveMany(ctx context.Context, mode models.SaveMode, devices if mode == 0 || mode&models.SMState != 0 { _, err = tx.NamedExecContext(ctx, ` - REPLACE INTO device_state(device_id, hue, saturation, kelvin, power, intensity, temperature) - VALUES (:device_id, :hue, :saturation, :kelvin, :power, :intensity, :temperature) + REPLACE INTO device_state(device_id, hue, saturation, kelvin, power, intensity, color, temperature) + VALUES (:device_id, :hue, :saturation, :kelvin, :power, :intensity, :color, :temperature) `, deviceStateRecord{ DeviceID: record.ID, - Hue: device.State.Color.Hue, - Saturation: device.State.Color.Saturation, - Kelvin: device.State.Color.Kelvin, + Hue: 40, + Saturation: 0, + Kelvin: 0, + Color: device.State.Color.String(), Power: device.State.Power, Intensity: device.State.Intensity, Temperature: device.State.Temperature, @@ -373,14 +375,12 @@ func (r *DeviceRepo) populate(ctx context.Context, records []deviceRecord) ([]mo for _, state := range states { if state.DeviceID == record.ID { + color, _ := models.ParseColorValue(state.Color) + device.State = models.DeviceState{ - Power: state.Power, - Color: models.ColorValue{ - Hue: state.Hue, - Saturation: state.Saturation, - Kelvin: state.Kelvin, - }, - Intensity: state.Intensity, + Power: state.Power, + Color: color, + Intensity: state.Intensity, Temperature: state.Temperature, } } diff --git a/internal/mysql/presetrepo.go b/internal/mysql/presetrepo.go index 296fcc1..221307f 100644 --- a/internal/mysql/presetrepo.go +++ b/internal/mysql/presetrepo.go @@ -12,6 +12,7 @@ type presetRecord struct { Hue float64 `db:"hue"` Saturation float64 `db:"saturation"` Kelvin int `db:"kelvin"` + Value string `db:"value"` } type ColorPresetRepo struct { @@ -42,8 +43,8 @@ func (c *ColorPresetRepo) Save(ctx context.Context, preset *models.ColorPreset) if preset.ID > 0 { _, err := c.DBX.ExecContext( ctx, - "UPDATE color_preset SET name = ?, hue = ?, saturation = ?, kelvin = ? WHERE id = ?", - preset.Name, preset.Value.Hue, preset.Value.Saturation, preset.Value.Kelvin, preset.ID, + "UPDATE color_preset SET name = ?, value = ? WHERE id = ?", + preset.Name, preset.Value.String(), preset.ID, ) if err != nil { @@ -52,8 +53,8 @@ func (c *ColorPresetRepo) Save(ctx context.Context, preset *models.ColorPreset) } else { rs, err := c.DBX.ExecContext( ctx, - "INSERT INTO color_preset (name, hue, saturation, kelvin) VALUES (?, ?, ?, ?)", - preset.Name, preset.Value.Hue, preset.Value.Saturation, preset.Value.Kelvin, + "INSERT INTO color_preset (name, value, hue, saturation, kelvin) VALUES (?, ?, 0, 0, 0)", + preset.Name, preset.Value.String(), ) if err != nil { @@ -84,14 +85,12 @@ func (c *ColorPresetRepo) Delete(ctx context.Context, preset *models.ColorPreset func (c *ColorPresetRepo) fromRecords(records ...presetRecord) []models.ColorPreset { newList := make([]models.ColorPreset, len(records), len(records)) for i, record := range records { + color, _ := models.ParseColorValue(record.Value) + newList[i] = models.ColorPreset{ - ID: record.ID, - Name: record.Name, - Value: models.ColorValue{ - Hue: record.Hue, - Saturation: record.Saturation, - Kelvin: record.Kelvin, - }, + ID: record.ID, + Name: record.Name, + Value: color, } } diff --git a/models/colorhs.go b/models/colorhs.go new file mode 100644 index 0000000..8410752 --- /dev/null +++ b/models/colorhs.go @@ -0,0 +1,17 @@ +package models + +import "github.com/lucasb-eyer/go-colorful" + +type ColorHS struct { + Hue float64 `json:"hue"` + Sat float64 `json:"sat"` +} + +func (hs ColorHS) ToXY() ColorXY { + return hs.ToRGB().ToXY() +} + +func (hs ColorHS) ToRGB() ColorRGB { + c := colorful.Hsv(hs.Hue, hs.Sat, 1) + return ColorRGB{Red: c.R, Green: c.G, Blue: c.B} +} diff --git a/models/colorrgb.go b/models/colorrgb.go new file mode 100644 index 0000000..cd0e300 --- /dev/null +++ b/models/colorrgb.go @@ -0,0 +1,29 @@ +package models + +import "github.com/lucasb-eyer/go-colorful" + +type ColorRGB struct { + Red float64 `json:"red"` + Green float64 `json:"green"` + Blue float64 `json:"blue"` +} + +func (rgb ColorRGB) AtIntensity(intensity float64) ColorRGB { + hue, sat, _ := colorful.Color{R: rgb.Red, G: rgb.Green, B: rgb.Blue}.Hsv() + hsv2 := colorful.Hsv(hue, sat, intensity) + return ColorRGB{Red: hsv2.R, Green: hsv2.G, Blue: hsv2.B} +} + +func (rgb ColorRGB) ToHS() ColorHS { + hue, sat, _ := colorful.Color{R: rgb.Red, G: rgb.Green, B: rgb.Blue}.Hsv() + return ColorHS{Hue: hue, Sat: sat} +} + +func (rgb ColorRGB) ToXY() ColorXY { + x, y, z := (colorful.Color{R: rgb.Red, G: rgb.Green, B: rgb.Blue}).Xyz() + + return ColorXY{ + X: x / (x + y + z), + Y: y / (x + y + z), + } +} diff --git a/models/colorvalue.go b/models/colorvalue.go index 68ae594..c2eda95 100644 --- a/models/colorvalue.go +++ b/models/colorvalue.go @@ -2,62 +2,219 @@ package models import ( "fmt" - "math" + "github.com/lucasb-eyer/go-colorful" "strconv" "strings" ) type ColorValue struct { - Hue float64 `json:"h,omitempty"` // 0..360 - Saturation float64 `json:"s,omitempty"` // 0..=1 - Kelvin int `json:"kelvin,omitempty"` - XY *ColorXY `json:"xy,omitempty"` + RGB *ColorRGB `json:"rgb,omitempty"` + HS *ColorHS `json:"hs,omitempty"` + K *int `json:"k,omitempty"` + XY *ColorXY `json:"xy,omitempty"` } -func (c *ColorValue) IsEmpty() bool { - return c.XY == nil && c.Kelvin == 0 && c.Saturation == 0 && c.Hue == 0 +func (cv *ColorValue) IsHueSat() bool { + return cv.HS != nil } -func (c *ColorValue) IsHueSat() bool { - return !c.IsKelvin() +func (cv *ColorValue) IsHueSatKelvin() bool { + return cv.HS != nil && cv.K != nil } -func (c *ColorValue) IsKelvin() bool { - return !c.IsXY() && c.Kelvin > 0 +func (cv *ColorValue) IsKelvin() bool { + return cv.K != nil } -func (c *ColorValue) IsXY() bool { - return c.XY != nil +func (cv *ColorValue) IsEmpty() bool { + return *cv == ColorValue{} } -// ToXY converts the color to XY if possible. If the color already is XY, it returns -// a copy of its held value. There are no guarantees of conforming to a gamut, however. -func (c *ColorValue) ToXY() (xy ColorXY, ok bool) { - if c.XY != nil { - xy = *c.XY +func (cv *ColorValue) SetK(k int) { + *cv = ColorValue{K: &k} +} + +func (cv *ColorValue) SetXY(xy ColorXY) { + *cv = ColorValue{XY: &xy} +} + +// ToRGB tries to copy the color to an RGB color. If it's already RGB, it will be plainly copied, but HS +// will be returned. If ok is false, then no copying has occurred and cv2 will be blank. +func (cv *ColorValue) ToRGB() (cv2 ColorValue, ok bool) { + if cv.RGB != nil { + rgb := *cv.RGB + cv2 = ColorValue{RGB: &rgb} ok = true - } else if c.Kelvin > 0 && c.Hue < 0.001 && c.Saturation <= 0.001 { - ok = false - } else { - xy = hsToXY(c.Hue, c.Saturation) + } else if cv.HS != nil { + rgb := cv.HS.ToRGB() + cv2 = ColorValue{RGB: &rgb} + ok = true + } else if cv.XY != nil { + rgb := cv.XY.ToRGB() + cv2 = ColorValue{RGB: &rgb} ok = true } return } -func (c *ColorValue) String() string { - if c.Kelvin > 0 { - return fmt.Sprintf("k:%d", c.Kelvin) +func (cv *ColorValue) ToHS() (cv2 ColorValue, ok bool) { + if cv.HS != nil { + hs := *cv.HS + cv2 = ColorValue{HS: &hs} + ok = true + } else if cv.RGB != nil { + hs := cv.RGB.ToHS() + cv2 = ColorValue{HS: &hs} + ok = true + } else if cv.XY != nil { + hs := cv.XY.ToHS() + cv2 = ColorValue{HS: &hs} + ok = true } - return fmt.Sprintf("hs:%.4g,%.3g", c.Hue, c.Saturation) + return } -func ParseColorValue(raw string) (ColorValue, error) { +func (cv *ColorValue) ToHSK() (cv2 ColorValue, ok bool) { + k := 4000 + + if cv.HS != nil { + hs := *cv.HS + cv2 = ColorValue{HS: &hs} + + if cv.K != nil { + k = *cv.K + } + cv2.K = &k + + ok = true + } else if cv.RGB != nil { + hs := cv.RGB.ToHS() + cv2 = ColorValue{HS: &hs} + cv2.K = &k + ok = true + } else if cv.XY != nil { + hs := cv.XY.ToHS() + cv2 = ColorValue{HS: &hs} + cv2.K = &k + ok = true + } else if cv.K != nil { + k = *cv.K + cv2.HS = &ColorHS{Hue: 0, Sat: 0} + cv2.K = &k + ok = true + } + + return +} + +// ToXY tries to copy the color to an XY color. +func (cv *ColorValue) ToXY() (cv2 ColorValue, ok bool) { + if cv.XY != nil { + xy := *cv.XY + cv2 = ColorValue{XY: &xy} + ok = true + } else if cv.HS != nil { + xy := cv.HS.ToXY() + cv2 = ColorValue{XY: &xy} + ok = true + } else if cv.RGB != nil { + xy := cv.RGB.ToXY() + cv2 = ColorValue{XY: &xy} + ok = true + } + + return +} + +func (cv *ColorValue) colorful() colorful.Color { + switch { + case cv.HS != nil: + return colorful.Hsv(cv.HS.Hue, cv.HS.Sat, 1) + case cv.RGB != nil: + return colorful.Color{R: cv.RGB.Red, G: cv.RGB.Green, B: cv.RGB.Blue} + case cv.XY != nil: + return colorful.Xyy(cv.XY.X, cv.XY.Y, 0.5) + default: + return colorful.Color{R: 255, B: 255, G: 255} + } +} + +func (cv *ColorValue) Interpolate(other ColorValue, fac float64) ColorValue { + // Special case for kelvin values. + if cv.IsKelvin() && other.IsKelvin() { + k1 := *cv.K + k2 := *cv.K + k3 := k1 + int(float64(k2-k1)*fac) + return ColorValue{K: &k3} + } + + // Get the colorful values. + cvCF := cv.colorful() + otherCF := other.colorful() + + // Blend and normalize + blended := cvCF.BlendLuv(otherCF, fac) + blendedHue, blendedSat, _ := blended.Hsv() + blendedHs := ColorHS{Hue: blendedHue, Sat: blendedSat} + + // Convert to the first's type + switch cv.Kind() { + case "rgb": + rgb := blendedHs.ToRGB() + return ColorValue{RGB: &rgb} + case "xy": + xy := blendedHs.ToXY() + return ColorValue{XY: &xy} + default: + return ColorValue{HS: &blendedHs} + } +} + +func (cv *ColorValue) Kind() string { + switch { + case cv.RGB != nil: + return "rgb" + case cv.XY != nil: + return "xy" + case cv.HS != nil && cv.K != nil: + return "hsk" + case cv.HS != nil: + return "hs" + case cv.K != nil: + return "k" + default: + return "" + } +} + +func (cv *ColorValue) String() string { + switch { + case cv.RGB != nil: + return fmt.Sprintf("rgb:%.3f,%.3f,%.3f", cv.RGB.Red, cv.RGB.Green, cv.RGB.Blue) + case cv.XY != nil: + return fmt.Sprintf("xy:%.4f,%.4f", cv.XY.X, cv.XY.Y) + case cv.HS != nil && cv.K != nil: + return fmt.Sprintf("hsk:%.4f,%.3f,%d", cv.HS.Hue, cv.HS.Sat, *cv.K) + case cv.HS != nil: + return fmt.Sprintf("hs:%.4f,%.3f", cv.HS.Hue, cv.HS.Sat) + case cv.K != nil: + return fmt.Sprintf("k:%d", *cv.K) + default: + return "" + } +} + +func ParseColorValue(raw string) (cv2 ColorValue, err error) { + if raw == "" { + return + } + tokens := strings.SplitN(raw, ":", 2) if len(tokens) != 2 { - return ColorValue{}, ErrBadInput + err = ErrBadInput + return } switch tokens[0] { @@ -65,65 +222,147 @@ func ParseColorValue(raw string) (ColorValue, error) { { parsedPart, err := strconv.Atoi(tokens[1]) if err != nil { - return ColorValue{}, ErrBadInput + err = ErrBadInput + break } - return ColorValue{Kelvin: parsedPart}, nil + cv2.K = &parsedPart } case "xy": { parts := strings.Split(tokens[1], ",") if len(parts) < 2 { - return ColorValue{}, ErrUnknownColorFormat + err = ErrUnknownColorFormat + return } x, err1 := strconv.ParseFloat(parts[0], 64) y, err2 := strconv.ParseFloat(parts[1], 64) if err1 != nil || err2 != nil { - return ColorValue{}, ErrBadInput + err = ErrBadInput + break } - return ColorValue{XY: &ColorXY{X: x, Y: y}}, nil + cv2.XY = &ColorXY{x, y} } case "hs": { parts := strings.Split(tokens[1], ",") if len(parts) < 2 { - return ColorValue{}, ErrUnknownColorFormat + err = ErrUnknownColorFormat + return } part1, err1 := strconv.ParseFloat(parts[0], 64) part2, err2 := strconv.ParseFloat(parts[1], 64) if err1 != nil || err2 != nil { - return ColorValue{}, ErrBadInput + err = ErrBadInput + break } - return ColorValue{Hue: math.Mod(part1, 360), Saturation: math.Min(math.Max(part2, 0), 1)}, nil + cv2.HS = &ColorHS{Hue: part1, Sat: part2} } case "hsk": { parts := strings.Split(tokens[1], ",") if len(parts) < 3 { - return ColorValue{}, ErrUnknownColorFormat + err = ErrUnknownColorFormat + return } part1, err1 := strconv.ParseFloat(parts[0], 64) part2, err2 := strconv.ParseFloat(parts[1], 64) part3, err3 := strconv.Atoi(parts[2]) if err1 != nil || err2 != nil || err3 != nil { - return ColorValue{}, ErrBadInput + err = ErrBadInput + break + } + + cv2.HS = &ColorHS{Hue: part1, Sat: part2} + cv2.K = &part3 + } + + case "rgb": + { + if strings.HasPrefix(tokens[1], "#") { + hex := tokens[1][1:] + if !validHex(hex) { + err = ErrBadInput + break + } + + if len(hex) == 6 { + cv2.RGB = &ColorRGB{ + Red: float64(hex2num(hex[0:2])) / 255.0, + Green: float64(hex2num(hex[2:4])) / 255.0, + Blue: float64(hex2num(hex[4:6])) / 255.0, + } + } else if len(hex) == 3 { + cv2.RGB = &ColorRGB{ + Red: float64(hex2digit(hex[0])) / 15.0, + Green: float64(hex2digit(hex[1])) / 15.0, + Blue: float64(hex2digit(hex[2])) / 15.0, + } + } else { + err = ErrUnknownColorFormat + return + } + } else { + parts := strings.Split(tokens[1], ",") + if len(parts) < 3 { + err = ErrUnknownColorFormat + return + } + + part1, err1 := strconv.ParseFloat(parts[0], 64) + part2, err2 := strconv.ParseFloat(parts[1], 64) + part3, err3 := strconv.ParseFloat(parts[2], 64) + if err1 != nil || err2 != nil || err3 != nil { + err = ErrBadInput + break + } + + cv2.RGB = &ColorRGB{Red: part1, Green: part2, Blue: part3} } - return ColorValue{ - Hue: math.Mod(part1, 360), - Saturation: math.Min(math.Max(part2, 0), 1), - Kelvin: part3, - }, nil + normalizedRGB := cv2.RGB.ToHS().ToRGB() + cv2.RGB = &normalizedRGB + } + + default: + err = ErrUnknownColorFormat + } + + return +} + +func validHex(h string) bool { + for _, ch := range h { + if !((ch >= 'a' && ch <= 'f') || (ch >= '0' || ch <= '9')) { + return false } } - return ColorValue{}, ErrUnknownColorFormat + return true +} + +func hex2num(s string) int { + v := 0 + for _, h := range s { + v *= 16 + v += hex2digit(byte(h)) + } + + return v +} + +func hex2digit(h byte) int { + if h >= 'a' && h <= 'f' { + return 10 + int(h-'a') + } else { + return int(h - '0') + } } diff --git a/models/colorxy.go b/models/colorxy.go index 9b69d5d..903c95b 100644 --- a/models/colorxy.go +++ b/models/colorxy.go @@ -159,6 +159,17 @@ type ColorXY struct { Y float64 `json:"y"` } +func (xy ColorXY) ToRGB() ColorRGB { + h, s, _ := colorful.Xyy(xy.X, xy.Y, 0.5).Hsv() + c := colorful.Hsv(h, s, 1) + return ColorRGB{Red: c.R, Green: c.G, Blue: c.B} +} + +func (xy ColorXY) ToHS() ColorHS { + h, s, _ := colorful.Xyy(xy.X, xy.Y, 0.5).Hsv() + return ColorHS{Hue: h, Sat: s} +} + func (xy ColorXY) DistanceTo(other ColorXY) float64 { return math.Sqrt(math.Pow(xy.X-other.X, 2) + math.Pow(xy.Y-other.Y, 2)) } @@ -169,36 +180,3 @@ func (xy ColorXY) Round() ColorXY { Y: math.Round(xy.Y*10000) / 10000, } } - -func hsToXY(hue, sat float64) ColorXY { - c := colorful.Hsv(hue, sat, 1) - red255, green255, blue255 := c.RGB255() - red := float64(red255) / 255.0 - green := float64(green255) / 255.0 - blue := float64(blue255) / 255.0 - - return rgbToXY(red, green, blue) -} - -func rgbToXY(red float64, green float64, blue float64) ColorXY { - x := red*0.649926 + green*0.103455 + blue*0.197109 - y := red*0.234327 + green*0.743075 + blue*0.022598 - z := green*0.053077 + blue*1.035763 - - return ColorXY{ - X: x / (x + y + z), - Y: y / (x + y + z), - } -} - -func screenRGBToXY(red, green, blue float64) ColorXY { - for _, component := range []*float64{&red, &green, &blue} { - if *component > 0.04045 { - *component = math.Pow((*component+0.055)/(1.055), 2.4) - } else { - *component /= 12.92 - } - } - - return rgbToXY(red, green, blue) -} diff --git a/models/device.go b/models/device.go index b8cc0d3..c171b38 100644 --- a/models/device.go +++ b/models/device.go @@ -87,6 +87,7 @@ var ( DCColorHSK DeviceCapability = "ColorHSK" DCColorKelvin DeviceCapability = "ColorKelvin" DCColorXY DeviceCapability = "ColorXY" + DCColorRGB DeviceCapability = "ColorRGB" DCButtons DeviceCapability = "Buttons" DCPresence DeviceCapability = "Presence" DCIntensity DeviceCapability = "Intensity" @@ -190,7 +191,14 @@ func (d *Device) SetState(newState NewDeviceState) error { return err } - if (parsed.IsKelvin() && d.HasCapability(DCColorKelvin, DCColorHSK)) || (parsed.IsHueSat() && d.HasCapability(DCColorHS)) { + switch { + case (parsed.RGB != nil || parsed.XY != nil || parsed.HS != nil) || d.HasCapability(DCColorHS, DCColorXY, DCColorRGB): + d.State.Color = parsed + case parsed.K != nil && d.HasCapability(DCColorKelvin, DCColorHSK): + if !d.HasCapability(DCColorKelvin) { + d.State.Color.HS = &ColorHS{Hue: 0, Sat: 0} + } + d.State.Color = parsed } } @@ -232,11 +240,7 @@ func (s *NewDeviceState) Interpolate(other NewDeviceState, fac float64) NewDevic sc, err := ParseColorValue(*s.Color) oc, err2 := ParseColorValue(*other.Color) if err == nil && err2 == nil { - rc := ColorValue{} - rc.Hue = interpolateFloat(sc.Hue, oc.Hue, fac) - rc.Saturation = interpolateFloat(sc.Saturation, oc.Saturation, fac) - rc.Kelvin = interpolateInt(sc.Kelvin, oc.Kelvin, fac) - + rc := sc.Interpolate(oc, fac) rcStr := rc.String() n.Color = &rcStr } diff --git a/scripts/20220220121712_color_preset_wipe.sql b/scripts/20220220121712_color_preset_wipe.sql new file mode 100644 index 0000000..17121ab --- /dev/null +++ b/scripts/20220220121712_color_preset_wipe.sql @@ -0,0 +1,9 @@ +-- +goose Up +-- +goose StatementBegin +TRUNCATE color_preset; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +SELECT 'down SQL query'; +-- +goose StatementEnd diff --git a/scripts/20220220121724_color_preset_add_column_value.sql b/scripts/20220220121724_color_preset_add_column_value.sql new file mode 100644 index 0000000..2c3f904 --- /dev/null +++ b/scripts/20220220121724_color_preset_add_column_value.sql @@ -0,0 +1,9 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE color_preset ADD COLUMN value VARCHAR(255) NOT NULL DEFAULT 'hs:0,0'; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE color_preset DROP COLUMN value; +-- +goose StatementEnd diff --git a/scripts/20220220124631_device_state_color.sql b/scripts/20220220124631_device_state_color.sql new file mode 100644 index 0000000..ada8f3e --- /dev/null +++ b/scripts/20220220124631_device_state_color.sql @@ -0,0 +1,9 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE device_state ADD COLUMN color VARCHAR(255) NOT NULL DEFAULT 'hs:0,0'; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE device_state DROP COLUMN IF EXISTS color; +-- +goose StatementEnd