From 62e7cb88f40262c22c10c2a7b38736cb0db91003 Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Mon, 28 Feb 2022 22:52:06 +0100 Subject: [PATCH] refactor color system to be separate from models, also move errors out of models. --- app/api/devices.go | 3 +- app/api/presets.go | 7 +- app/api/util.go | 28 +- cmd/bridgetest/main.go | 3 +- cmd/xy/main.go | 10 +- internal/color/color.go | 369 ++++++++++++++++++++ models/colorhs.go => internal/color/hs.go | 10 +- models/colorrgb.go => internal/color/rgb.go | 16 +- models/colorxy.go => internal/color/xy.go | 50 +-- internal/drivers/hue/bridge.go | 5 +- internal/drivers/hue/driver.go | 3 +- internal/drivers/hue2/bridge.go | 4 +- internal/drivers/hue2/client.go | 4 +- internal/drivers/hue2/data.go | 10 +- internal/drivers/hue2/driver.go | 3 +- internal/drivers/lifx/bridge.go | 12 +- internal/drivers/lifx/client.go | 6 +- internal/drivers/lifx/packet.go | 6 +- internal/drivers/lifx/payloads.go | 14 +- internal/drivers/lifx/state.go | 13 +- internal/drivers/mill/bridge.go | 19 +- internal/drivers/nanoleaf/bridge.go | 10 +- internal/drivers/nanoleaf/driver.go | 5 +- internal/drivers/provider.go | 8 +- {models => internal/lerrors}/errors.go | 2 +- internal/mysql/devicerepo.go | 3 +- internal/mysql/presetrepo.go | 3 +- internal/mysql/util.go | 6 +- models/colorpreset.go | 7 +- models/colorvalue.go | 368 ------------------- models/device.go | 26 +- models/scene.go | 15 +- 32 files changed, 532 insertions(+), 516 deletions(-) create mode 100644 internal/color/color.go rename models/colorhs.go => internal/color/hs.go (51%) rename models/colorrgb.go => internal/color/rgb.go (61%) rename models/colorxy.go => internal/color/xy.go (75%) rename {models => internal/lerrors}/errors.go (99%) delete mode 100644 models/colorvalue.go diff --git a/app/api/devices.go b/app/api/devices.go index afba0c6..e77e98b 100644 --- a/app/api/devices.go +++ b/app/api/devices.go @@ -4,6 +4,7 @@ import ( "context" "git.aiterp.net/lucifer/new-server/app/config" "git.aiterp.net/lucifer/new-server/app/services/publisher" + "git.aiterp.net/lucifer/new-server/internal/lerrors" "git.aiterp.net/lucifer/new-server/models" "github.com/gin-gonic/gin" "log" @@ -305,7 +306,7 @@ func Devices(r gin.IRoutes) { forgettableDriver, ok := driver.(models.ForgettableDriver) if !ok { - return nil, models.ErrCannotForget + return nil, lerrors.ErrCannotForget } err = forgettableDriver.ForgetDevice(ctxOf(c), bridge, *device) if err != nil { diff --git a/app/api/presets.go b/app/api/presets.go index 2b443da..156f3e4 100644 --- a/app/api/presets.go +++ b/app/api/presets.go @@ -2,6 +2,7 @@ package api import ( "git.aiterp.net/lucifer/new-server/app/config" + "git.aiterp.net/lucifer/new-server/internal/color" "git.aiterp.net/lucifer/new-server/models" "github.com/gin-gonic/gin" ) @@ -25,13 +26,13 @@ func ColorPresets(r gin.IRoutes) { return nil, err } - newColor, err := models.ParseColorValue(body.ColorString) + newColor, err := color.Parse(body.ColorString) if err != nil { return nil, err } preset := models.ColorPreset{ - Name: body.Name, + Name: body.Name, Value: newColor, } @@ -64,7 +65,7 @@ func ColorPresets(r gin.IRoutes) { } if body.ColorString != nil { - newColor, err := models.ParseColorValue(*body.ColorString) + newColor, err := color.Parse(*body.ColorString) if err != nil { return nil, err } diff --git a/app/api/util.go b/app/api/util.go index 5cdece7..3b8feed 100644 --- a/app/api/util.go +++ b/app/api/util.go @@ -3,25 +3,25 @@ package api import ( "context" "encoding/json" - "git.aiterp.net/lucifer/new-server/models" + "git.aiterp.net/lucifer/new-server/internal/lerrors" "github.com/gin-gonic/gin" "strconv" ) var errorMap = map[error]int{ - models.ErrNotFound: 404, - models.ErrInvalidName: 400, - models.ErrBadInput: 400, - models.ErrBadColor: 400, - models.ErrInternal: 500, - models.ErrUnknownColorFormat: 400, + lerrors.ErrNotFound: 404, + lerrors.ErrInvalidName: 400, + lerrors.ErrBadInput: 400, + lerrors.ErrBadColor: 400, + lerrors.ErrInternal: 500, + lerrors.ErrUnknownColorFormat: 400, - models.ErrSceneInvalidInterval: 400, - models.ErrSceneNoRoles: 400, - models.ErrSceneRoleNoStates: 400, - models.ErrSceneRoleUnsupportedOrdering: 422, - models.ErrSceneRoleUnknownEffect: 422, - models.ErrSceneRoleUnknownPowerMode: 422, + lerrors.ErrSceneInvalidInterval: 400, + lerrors.ErrSceneNoRoles: 400, + lerrors.ErrSceneRoleNoStates: 400, + lerrors.ErrSceneRoleUnsupportedOrdering: 422, + lerrors.ErrSceneRoleUnknownEffect: 422, + lerrors.ErrSceneRoleUnknownPowerMode: 422, } type response struct { @@ -66,7 +66,7 @@ func intParam(c *gin.Context, key string) int { func parseBody(c *gin.Context, target interface{}) error { err := json.NewDecoder(c.Request.Body).Decode(target) if err != nil { - return models.ErrBadInput + return lerrors.ErrBadInput } return nil diff --git a/cmd/bridgetest/main.go b/cmd/bridgetest/main.go index 9670c4d..7b49e6e 100644 --- a/cmd/bridgetest/main.go +++ b/cmd/bridgetest/main.go @@ -7,6 +7,7 @@ import ( "flag" "fmt" "git.aiterp.net/lucifer/new-server/app/config" + "git.aiterp.net/lucifer/new-server/internal/color" "git.aiterp.net/lucifer/new-server/models" "log" "os" @@ -124,7 +125,7 @@ func main() { continue } - color, err := models.ParseColorValue(tokens[2]) + color, err := color.Parse(tokens[2]) if err != nil { _, _ = fmt.Fprintln(os.Stderr, "Invalid color:", err) continue diff --git a/cmd/xy/main.go b/cmd/xy/main.go index 7ce6799..b254361 100644 --- a/cmd/xy/main.go +++ b/cmd/xy/main.go @@ -1,21 +1,15 @@ package main import ( - "encoding/json" - "git.aiterp.net/lucifer/new-server/models" + "git.aiterp.net/lucifer/new-server/internal/color" "log" ) func main() { - cv, _ := models.ParseColorValue("rgb:#fff") + cv, _ := color.Parse("xy:0.1944,0.0942") hs, _ := cv.ToHS() log.Println(cv.String()) log.Println(hs.String()) xy, _ := hs.ToXY() log.Println(xy.String()) } - -func toJSON(v interface{}) string { - j, _ := json.Marshal(v) - return string(j) -} diff --git a/internal/color/color.go b/internal/color/color.go new file mode 100644 index 0000000..3cdd8f4 --- /dev/null +++ b/internal/color/color.go @@ -0,0 +1,369 @@ +package color + +import ( + "fmt" + "git.aiterp.net/lucifer/new-server/internal/lerrors" + "github.com/lucasb-eyer/go-colorful" + "strconv" + "strings" +) + +type Color struct { + RGB *RGB `json:"rgb,omitempty"` + HS *HueSat `json:"hs,omitempty"` + K *int `json:"k,omitempty"` + XY *XY `json:"xy,omitempty"` +} + +func (col *Color) IsHueSat() bool { + return col.HS != nil +} + +func (col *Color) IsHueSatKelvin() bool { + return col.HS != nil && col.K != nil +} + +func (col *Color) IsKelvin() bool { + return col.K != nil +} + +func (col *Color) IsEmpty() bool { + return *col == Color{} +} + +func (col *Color) SetK(k int) { + *col = Color{K: &k} +} + +func (col *Color) SetXY(xy XY) { + *col = Color{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 (col *Color) ToRGB() (col2 Color, ok bool) { + if col.RGB != nil { + rgb := *col.RGB + col2 = Color{RGB: &rgb} + ok = true + } else if col.HS != nil { + rgb := col.HS.ToRGB() + col2 = Color{RGB: &rgb} + ok = true + } else if col.XY != nil { + rgb := col.XY.ToRGB() + col2 = Color{RGB: &rgb} + ok = true + } + + return +} + +func (col *Color) ToHS() (col2 Color, ok bool) { + if col.HS != nil { + hs := *col.HS + col2 = Color{HS: &hs} + ok = true + } else if col.RGB != nil { + hs := col.RGB.ToHS() + col2 = Color{HS: &hs} + ok = true + } else if col.XY != nil { + hs := col.XY.ToHS() + col2 = Color{HS: &hs} + ok = true + } + + return +} + +func (col *Color) ToHSK() (col2 Color, ok bool) { + k := 4000 + + if col.HS != nil { + hs := *col.HS + col2 = Color{HS: &hs} + + if col.K != nil { + k = *col.K + } + col2.K = &k + + ok = true + } else if col.RGB != nil { + hs := col.RGB.ToHS() + col2 = Color{HS: &hs} + col2.K = &k + ok = true + } else if col.XY != nil { + hs := col.XY.ToHS() + col2 = Color{HS: &hs} + col2.K = &k + ok = true + } else if col.K != nil { + k = *col.K + col2.HS = &HueSat{Hue: 0, Sat: 0} + col2.K = &k + ok = true + } + + return +} + +// ToXY tries to copy the color to an XY color. +func (col *Color) ToXY() (col2 Color, ok bool) { + if col.XY != nil { + xy := *col.XY + col2 = Color{XY: &xy} + ok = true + } else if col.HS != nil { + xy := col.HS.ToXY() + col2 = Color{XY: &xy} + ok = true + } else if col.RGB != nil { + xy := col.RGB.ToXY() + col2 = Color{XY: &xy} + ok = true + } + + return +} + +func (col *Color) Interpolate(other Color, fac float64) Color { + // Special case for kelvin values. + if col.IsKelvin() && other.IsKelvin() { + k1 := *col.K + k2 := *col.K + k3 := k1 + int(float64(k2-k1)*fac) + return Color{K: &k3} + } + + // Get the colorful values. + cvCF := col.colorful() + otherCF := other.colorful() + + // Blend and normalize + blended := cvCF.BlendLuv(otherCF, fac) + blendedHue, blendedSat, _ := blended.Hsv() + blendedHs := HueSat{Hue: blendedHue, Sat: blendedSat} + + // Convert to the first's type + switch col.Kind() { + case "rgb": + rgb := blendedHs.ToRGB() + return Color{RGB: &rgb} + case "xy": + xy := blendedHs.ToXY() + return Color{XY: &xy} + default: + return Color{HS: &blendedHs} + } +} + +func (col *Color) Kind() string { + switch { + case col.RGB != nil: + return "rgb" + case col.XY != nil: + return "xy" + case col.HS != nil && col.K != nil: + return "hsk" + case col.HS != nil: + return "hs" + case col.K != nil: + return "k" + default: + return "" + } +} + +func (col *Color) String() string { + switch { + case col.RGB != nil: + return fmt.Sprintf("rgb:%.3f,%.3f,%.3f", col.RGB.Red, col.RGB.Green, col.RGB.Blue) + case col.XY != nil: + return fmt.Sprintf("xy:%.4f,%.4f", col.XY.X, col.XY.Y) + case col.HS != nil && col.K != nil: + return fmt.Sprintf("hsk:%.4f,%.3f,%d", col.HS.Hue, col.HS.Sat, *col.K) + case col.HS != nil: + return fmt.Sprintf("hs:%.4f,%.3f", col.HS.Hue, col.HS.Sat) + case col.K != nil: + return fmt.Sprintf("k:%d", *col.K) + default: + return "" + } +} + +func (col *Color) colorful() colorful.Color { + switch { + case col.HS != nil: + return colorful.Hsv(col.HS.Hue, col.HS.Sat, 1) + case col.RGB != nil: + return colorful.Color{R: col.RGB.Red, G: col.RGB.Green, B: col.RGB.Blue} + case col.XY != nil: + return colorful.Xyy(col.XY.X, col.XY.Y, 0.5) + default: + return colorful.Color{R: 255, B: 255, G: 255} + } +} + +func Parse(raw string) (col Color, err error) { + if raw == "" { + return + } + + tokens := strings.SplitN(raw, ":", 2) + if len(tokens) != 2 { + err = lerrors.ErrBadInput + return + } + + switch tokens[0] { + case "kelvin", "k": + { + parsedPart, err := strconv.Atoi(tokens[1]) + if err != nil { + err = lerrors.ErrBadInput + break + } + + col.K = &parsedPart + } + + case "xy": + { + parts := strings.Split(tokens[1], ",") + if len(parts) < 2 { + err = lerrors.ErrUnknownColorFormat + return + } + + x, err1 := strconv.ParseFloat(parts[0], 64) + y, err2 := strconv.ParseFloat(parts[1], 64) + if err1 != nil || err2 != nil { + err = lerrors.ErrBadInput + break + } + + col.XY = &XY{x, y} + } + + case "hs": + { + parts := strings.Split(tokens[1], ",") + if len(parts) < 2 { + err = lerrors.ErrUnknownColorFormat + return + } + + part1, err1 := strconv.ParseFloat(parts[0], 64) + part2, err2 := strconv.ParseFloat(parts[1], 64) + if err1 != nil || err2 != nil { + err = lerrors.ErrBadInput + break + } + + col.HS = &HueSat{Hue: part1, Sat: part2} + } + + case "hsk": + { + parts := strings.Split(tokens[1], ",") + if len(parts) < 3 { + err = lerrors.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 { + err = lerrors.ErrBadInput + break + } + + col.HS = &HueSat{Hue: part1, Sat: part2} + col.K = &part3 + } + + case "rgb": + { + if strings.HasPrefix(tokens[1], "#") { + hex := tokens[1][1:] + if !validHex(hex) { + err = lerrors.ErrBadInput + break + } + + if len(hex) == 6 { + col.RGB = &RGB{ + 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 { + col.RGB = &RGB{ + Red: float64(hex2digit(hex[0])) / 15.0, + Green: float64(hex2digit(hex[1])) / 15.0, + Blue: float64(hex2digit(hex[2])) / 15.0, + } + } else { + err = lerrors.ErrUnknownColorFormat + return + } + } else { + parts := strings.Split(tokens[1], ",") + if len(parts) < 3 { + err = lerrors.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 = lerrors.ErrBadInput + break + } + + col.RGB = &RGB{Red: part1, Green: part2, Blue: part3} + } + + normalizedRGB := col.RGB.ToHS().ToRGB() + col.RGB = &normalizedRGB + } + + default: + err = lerrors.ErrUnknownColorFormat + } + + return +} + +func validHex(h string) bool { + for _, ch := range h { + if !((ch >= 'a' && ch <= 'f') || (ch >= '0' || ch <= '9')) { + return false + } + } + + 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/colorhs.go b/internal/color/hs.go similarity index 51% rename from models/colorhs.go rename to internal/color/hs.go index 8410752..be97786 100644 --- a/models/colorhs.go +++ b/internal/color/hs.go @@ -1,17 +1,17 @@ -package models +package color import "github.com/lucasb-eyer/go-colorful" -type ColorHS struct { +type HueSat struct { Hue float64 `json:"hue"` Sat float64 `json:"sat"` } -func (hs ColorHS) ToXY() ColorXY { +func (hs HueSat) ToXY() XY { return hs.ToRGB().ToXY() } -func (hs ColorHS) ToRGB() ColorRGB { +func (hs HueSat) ToRGB() RGB { c := colorful.Hsv(hs.Hue, hs.Sat, 1) - return ColorRGB{Red: c.R, Green: c.G, Blue: c.B} + return RGB{Red: c.R, Green: c.G, Blue: c.B} } diff --git a/models/colorrgb.go b/internal/color/rgb.go similarity index 61% rename from models/colorrgb.go rename to internal/color/rgb.go index cd0e300..5eb9878 100644 --- a/models/colorrgb.go +++ b/internal/color/rgb.go @@ -1,28 +1,28 @@ -package models +package color import "github.com/lucasb-eyer/go-colorful" -type ColorRGB struct { +type RGB struct { Red float64 `json:"red"` Green float64 `json:"green"` Blue float64 `json:"blue"` } -func (rgb ColorRGB) AtIntensity(intensity float64) ColorRGB { +func (rgb RGB) AtIntensity(intensity float64) RGB { 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} + return RGB{Red: hsv2.R, Green: hsv2.G, Blue: hsv2.B} } -func (rgb ColorRGB) ToHS() ColorHS { +func (rgb RGB) ToHS() HueSat { hue, sat, _ := colorful.Color{R: rgb.Red, G: rgb.Green, B: rgb.Blue}.Hsv() - return ColorHS{Hue: hue, Sat: sat} + return HueSat{Hue: hue, Sat: sat} } -func (rgb ColorRGB) ToXY() ColorXY { +func (rgb RGB) ToXY() XY { x, y, z := (colorful.Color{R: rgb.Red, G: rgb.Green, B: rgb.Blue}).Xyz() - return ColorXY{ + return XY{ X: x / (x + y + z), Y: y / (x + y + z), } diff --git a/models/colorxy.go b/internal/color/xy.go similarity index 75% rename from models/colorxy.go rename to internal/color/xy.go index 903c95b..d2665d0 100644 --- a/models/colorxy.go +++ b/internal/color/xy.go @@ -1,4 +1,4 @@ -package models +package color import ( "github.com/lucasb-eyer/go-colorful" @@ -8,17 +8,17 @@ import ( const eps = 0.0001 const epsSquare = eps * eps -type ColorGamut struct { - Red ColorXY `json:"red"` - Green ColorXY `json:"green"` - Blue ColorXY `json:"blue"` +type Gamut struct { + Red XY `json:"red"` + Green XY `json:"green"` + Blue XY `json:"blue"` } -func (cg *ColorGamut) side(x1, y1, x2, y2, x, y float64) float64 { +func (cg *Gamut) side(x1, y1, x2, y2, x, y float64) float64 { return (y2-y1)*(x-x1) + (-x2+x1)*(y-y1) } -func (cg *ColorGamut) naiveContains(color ColorXY) bool { +func (cg *Gamut) naiveContains(color XY) bool { x, y := color.X, color.Y x1, y1 := cg.Red.X, cg.Red.Y x2, y2 := cg.Green.X, cg.Green.Y @@ -31,7 +31,7 @@ func (cg *ColorGamut) naiveContains(color ColorXY) bool { return checkSide1 && checkSide2 && checkSide3 } -func (cg *ColorGamut) getBounds() (xMin, xMax, yMin, yMax float64) { +func (cg *Gamut) getBounds() (xMin, xMax, yMin, yMax float64) { x1, y1 := cg.Red.X, cg.Red.Y x2, y2 := cg.Green.X, cg.Green.Y x3, y3 := cg.Blue.X, cg.Blue.Y @@ -44,14 +44,14 @@ func (cg *ColorGamut) getBounds() (xMin, xMax, yMin, yMax float64) { return } -func (cg *ColorGamut) isInBounds(color ColorXY) bool { +func (cg *Gamut) isInBounds(color XY) bool { x, y := color.X, color.Y xMin, xMax, yMin, yMax := cg.getBounds() return !(x < xMin || xMax < x || y < yMin || yMax < y) } -func (cg *ColorGamut) distanceSquarePointToSegment(x1, y1, x2, y2, x, y float64) float64 { +func (cg *Gamut) distanceSquarePointToSegment(x1, y1, x2, y2, x, y float64) float64 { sqLength1 := (x2-x1)*(x2-x1) + (y2-y1)*(y2-y1) dotProduct := ((x-x1)*(x2-x1) + (y-y1)*(y2-y1)) / sqLength1 if dotProduct < 0 { @@ -64,7 +64,7 @@ func (cg *ColorGamut) distanceSquarePointToSegment(x1, y1, x2, y2, x, y float64) } } -func (cg *ColorGamut) atTheEdge(color ColorXY) bool { +func (cg *Gamut) atTheEdge(color XY) bool { x, y := color.X, color.Y x1, y1 := cg.Red.X, cg.Red.Y x2, y2 := cg.Green.X, cg.Green.Y @@ -83,7 +83,7 @@ func (cg *ColorGamut) atTheEdge(color ColorXY) bool { return false } -func (cg *ColorGamut) Contains(color ColorXY) bool { +func (cg *Gamut) Contains(color XY) bool { if cg == nil { return true } @@ -91,18 +91,18 @@ func (cg *ColorGamut) Contains(color ColorXY) bool { return cg.isInBounds(color) && (cg.naiveContains(color) || cg.atTheEdge(color)) } -func (cg *ColorGamut) Conform(color ColorXY) ColorXY { +func (cg *Gamut) Conform(color XY) XY { if cg.Contains(color) { return color } - var best *ColorXY + var best *XY xMin, xMax, yMin, yMax := cg.getBounds() for x := xMin; x < xMax; x += 0.001 { for y := yMin; y < yMax; y += 0.001 { - color2 := ColorXY{X: x, Y: y} + color2 := XY{X: x, Y: y} if cg.Contains(color2) { if best == nil || color.DistanceTo(color2) < color.DistanceTo(*best) { @@ -129,7 +129,7 @@ func (cg *ColorGamut) Conform(color ColorXY) ColorXY { for x := best.X - 0.001; x < best.X+0.001; x += 0.0002 { for y := best.Y - 0.001; y < best.Y+0.001; y += 0.0002 { - color2 := ColorXY{X: x, Y: y} + color2 := XY{X: x, Y: y} if cg.atTheEdge(color2) { if best == nil || color.DistanceTo(color2) < color.DistanceTo(*best) { @@ -141,7 +141,7 @@ func (cg *ColorGamut) Conform(color ColorXY) ColorXY { for x := best.X - 0.0001; x < best.X+0.0001; x += 0.00003 { for y := best.Y - 0.0001; y < best.Y+0.0001; y += 0.00003 { - color2 := ColorXY{X: x, Y: y} + color2 := XY{X: x, Y: y} if cg.atTheEdge(color2) { if best == nil || color.DistanceTo(color2) < color.DistanceTo(*best) { @@ -154,28 +154,28 @@ func (cg *ColorGamut) Conform(color ColorXY) ColorXY { return *best } -type ColorXY struct { +type XY struct { X float64 `json:"x"` Y float64 `json:"y"` } -func (xy ColorXY) ToRGB() ColorRGB { +func (xy XY) ToRGB() RGB { 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} + return RGB{Red: c.R, Green: c.G, Blue: c.B} } -func (xy ColorXY) ToHS() ColorHS { +func (xy XY) ToHS() HueSat { h, s, _ := colorful.Xyy(xy.X, xy.Y, 0.5).Hsv() - return ColorHS{Hue: h, Sat: s} + return HueSat{Hue: h, Sat: s} } -func (xy ColorXY) DistanceTo(other ColorXY) float64 { +func (xy XY) DistanceTo(other XY) float64 { return math.Sqrt(math.Pow(xy.X-other.X, 2) + math.Pow(xy.Y-other.Y, 2)) } -func (xy ColorXY) Round() ColorXY { - return ColorXY{ +func (xy XY) Round() XY { + return XY{ X: math.Round(xy.X*10000) / 10000, Y: math.Round(xy.Y*10000) / 10000, } diff --git a/internal/drivers/hue/bridge.go b/internal/drivers/hue/bridge.go index 0385e0a..5b53cc3 100644 --- a/internal/drivers/hue/bridge.go +++ b/internal/drivers/hue/bridge.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "git.aiterp.net/lucifer/new-server/internal/lerrors" "git.aiterp.net/lucifer/new-server/models" "golang.org/x/sync/errgroup" "io" @@ -159,7 +160,7 @@ func (b *Bridge) ForgetDevice(ctx context.Context, device models.Device) error { } b.mu.Unlock() if !found { - return models.ErrNotFound + return lerrors.ErrNotFound } // Delete light from bridge @@ -237,7 +238,7 @@ func (b *Bridge) getToken(ctx context.Context) (string, error) { return "", errLinkButtonNotPressed } if result[0].Success == nil { - return "", models.ErrUnexpectedResponse + return "", lerrors.ErrUnexpectedResponse } return result[0].Success.Username, nil diff --git a/internal/drivers/hue/driver.go b/internal/drivers/hue/driver.go index 2f1df8e..4da272f 100644 --- a/internal/drivers/hue/driver.go +++ b/internal/drivers/hue/driver.go @@ -5,6 +5,7 @@ import ( "encoding/json" "encoding/xml" "fmt" + "git.aiterp.net/lucifer/new-server/internal/lerrors" "git.aiterp.net/lucifer/new-server/models" "log" "net/http" @@ -22,7 +23,7 @@ type Driver struct { func (d *Driver) SearchBridge(ctx context.Context, address, _ string, dryRun bool) ([]models.Bridge, error) { if address == "" { if !dryRun { - return nil, models.ErrAddressOnlyDryRunnable + return nil, lerrors.ErrAddressOnlyDryRunnable } res, err := http.Get("https://discovery.meethue.com") diff --git a/internal/drivers/hue2/bridge.go b/internal/drivers/hue2/bridge.go index 3466721..e14edef 100644 --- a/internal/drivers/hue2/bridge.go +++ b/internal/drivers/hue2/bridge.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "git.aiterp.net/lucifer/new-server/internal/color" "git.aiterp.net/lucifer/new-server/models" "golang.org/x/sync/errgroup" "log" @@ -283,7 +284,6 @@ func (b *Bridge) MakeCongruent(ctx context.Context) (int, error) { } 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 } @@ -428,7 +428,7 @@ func (b *Bridge) GenerateDevices() []models.Device { } if light.Color != nil { if device.State.Color.IsEmpty() { - device.State.Color = models.ColorValue{ + device.State.Color = color.Color{ XY: &light.Color.XY, } } diff --git a/internal/drivers/hue2/client.go b/internal/drivers/hue2/client.go index a97cfa7..e666d41 100644 --- a/internal/drivers/hue2/client.go +++ b/internal/drivers/hue2/client.go @@ -8,7 +8,7 @@ import ( "encoding/json" "errors" "fmt" - "git.aiterp.net/lucifer/new-server/models" + "git.aiterp.net/lucifer/new-server/internal/lerrors" "io" "log" "net" @@ -52,7 +52,7 @@ func (c *Client) Register(ctx context.Context) (string, error) { } } if result[0].Success == nil { - return "", models.ErrUnexpectedResponse + return "", lerrors.ErrUnexpectedResponse } c.token = result[0].Success.Username diff --git a/internal/drivers/hue2/data.go b/internal/drivers/hue2/data.go index 7f7134b..e8bd6d6 100644 --- a/internal/drivers/hue2/data.go +++ b/internal/drivers/hue2/data.go @@ -4,7 +4,7 @@ import ( "encoding/json" "encoding/xml" "fmt" - "git.aiterp.net/lucifer/new-server/models" + "git.aiterp.net/lucifer/new-server/internal/color" "strings" "time" ) @@ -152,9 +152,9 @@ type LightDimming struct { } type LightColor struct { - Gamut models.ColorGamut `json:"gamut"` - GamutType string `json:"gamut_type"` - XY models.ColorXY `json:"xy"` + Gamut color.Gamut `json:"gamut"` + GamutType string `json:"gamut_type"` + XY color.XY `json:"xy"` } type LightCT struct { @@ -182,7 +182,7 @@ type LightAlert struct { type ResourceUpdate struct { Name *string Power *bool - ColorXY *models.ColorXY + ColorXY *color.XY Brightness *float64 Mirek *int TransitionDuration *time.Duration diff --git a/internal/drivers/hue2/driver.go b/internal/drivers/hue2/driver.go index d176ce5..e509967 100644 --- a/internal/drivers/hue2/driver.go +++ b/internal/drivers/hue2/driver.go @@ -5,6 +5,7 @@ import ( "encoding/json" "encoding/xml" "fmt" + "git.aiterp.net/lucifer/new-server/internal/lerrors" "git.aiterp.net/lucifer/new-server/models" "net/http" "sync" @@ -19,7 +20,7 @@ type Driver struct { func (d *Driver) SearchBridge(ctx context.Context, address, token string, dryRun bool) ([]models.Bridge, error) { if address == "" { if !dryRun { - return nil, models.ErrAddressOnlyDryRunnable + return nil, lerrors.ErrAddressOnlyDryRunnable } res, err := http.Get("https://discovery.meethue.com") diff --git a/internal/drivers/lifx/bridge.go b/internal/drivers/lifx/bridge.go index 9222999..d7b36a8 100644 --- a/internal/drivers/lifx/bridge.go +++ b/internal/drivers/lifx/bridge.go @@ -2,6 +2,8 @@ package lifx import ( "context" + "git.aiterp.net/lucifer/new-server/internal/color" + "git.aiterp.net/lucifer/new-server/internal/lerrors" "git.aiterp.net/lucifer/new-server/models" "log" "sync" @@ -20,7 +22,7 @@ type Bridge struct { func (b *Bridge) StartSearch(ctx context.Context) error { c := b.getClient() if c == nil { - return models.ErrBridgeRunningRequired + return lerrors.ErrBridgeRunningRequired } _, err := c.HorribleBroadcast(ctx, &GetService{}) @@ -129,9 +131,9 @@ func (b *Bridge) Run(ctx context.Context, debug bool) error { for { target, seq, payload, err := client.Recv(time.Millisecond * 200) - if err == models.ErrInvalidPacketSize || err == models.ErrPayloadTooShort || err == models.ErrUnrecognizedPacketType { + if err == lerrors.ErrInvalidPacketSize || err == lerrors.ErrPayloadTooShort || err == lerrors.ErrUnrecognizedPacketType { log.Println("LIFX udp socket received something weird:", err) - } else if err != nil && err != models.ErrReadTimeout { + } else if err != nil && err != lerrors.ErrReadTimeout { if ctx.Err() != nil { return ctx.Err() } @@ -168,8 +170,8 @@ func (b *Bridge) Run(ctx context.Context, debug bool) error { if state.deviceState == nil { state.deviceState = &models.DeviceState{ Power: p.On, - Color: models.ColorValue{ - HS: &models.ColorHS{ + Color: color.Color{ + HS: &color.HueSat{ Hue: p.Hue, Sat: p.Sat, }, diff --git a/internal/drivers/lifx/client.go b/internal/drivers/lifx/client.go index 63c862d..43e5a8e 100644 --- a/internal/drivers/lifx/client.go +++ b/internal/drivers/lifx/client.go @@ -4,7 +4,7 @@ import ( "context" "encoding/binary" "errors" - "git.aiterp.net/lucifer/new-server/models" + "git.aiterp.net/lucifer/new-server/internal/lerrors" "log" "math/rand" "net" @@ -215,7 +215,7 @@ func (c *Client) Recv(timeout time.Duration) (target string, seq uint8, payload } if err != nil { if netErr, ok := err.(*net.OpError); ok && netErr.Timeout() { - err = models.ErrReadTimeout + err = lerrors.ErrReadTimeout return } @@ -224,7 +224,7 @@ func (c *Client) Recv(timeout time.Duration) (target string, seq uint8, payload packet := Packet(c.buf[:n]) if n < 2 || packet.Size() != n && packet.Protocol() != 1024 { - err = models.ErrInvalidAddress + err = lerrors.ErrInvalidAddress return } diff --git a/internal/drivers/lifx/packet.go b/internal/drivers/lifx/packet.go index 365aa1c..ebe1e5a 100644 --- a/internal/drivers/lifx/packet.go +++ b/internal/drivers/lifx/packet.go @@ -3,7 +3,7 @@ package lifx import ( "encoding/binary" "fmt" - "git.aiterp.net/lucifer/new-server/models" + "git.aiterp.net/lucifer/new-server/internal/lerrors" "log" "net" ) @@ -60,7 +60,7 @@ func (p *Packet) SetTarget(v string) error { return err } if len(addr) != 6 { - return models.ErrInvalidAddress + return lerrors.ErrInvalidAddress } copy((*p)[8:], addr) @@ -133,7 +133,7 @@ func (p *Packet) Payload() (res Payload, err error) { res = &SetLightPower{} err = res.Decode((*p)[36:]) default: - err = models.ErrUnrecognizedPacketType + err = lerrors.ErrUnrecognizedPacketType } if err != nil { diff --git a/internal/drivers/lifx/payloads.go b/internal/drivers/lifx/payloads.go index 5e33a2a..9c3d484 100644 --- a/internal/drivers/lifx/payloads.go +++ b/internal/drivers/lifx/payloads.go @@ -3,7 +3,7 @@ package lifx import ( "encoding/binary" "fmt" - "git.aiterp.net/lucifer/new-server/models" + "git.aiterp.net/lucifer/new-server/internal/lerrors" "math" "time" ) @@ -82,7 +82,7 @@ type StateHostFirmware struct { func (p *StateHostFirmware) Decode(data []byte) error { if len(data) < 20 { - return models.ErrPayloadTooShort + return lerrors.ErrPayloadTooShort } ts := int64(binary.LittleEndian.Uint64(data[0:8])) @@ -123,7 +123,7 @@ type StateVersion struct { func (p *StateVersion) Decode(data []byte) error { if len(data) < 8 { - return models.ErrPayloadTooShort + return lerrors.ErrPayloadTooShort } p.Vendor = binary.LittleEndian.Uint32(data[0:4]) @@ -207,7 +207,7 @@ type SetColor struct { func (p *SetColor) Decode(data []byte) error { if len(data) < 13 { - return models.ErrPayloadTooShort + return lerrors.ErrPayloadTooShort } hue := binary.LittleEndian.Uint16(data[1:3]) @@ -263,7 +263,7 @@ type SetLightPower struct { func (p *SetLightPower) Decode(data []byte) error { if len(data) < 6 { - return models.ErrPayloadTooShort + return lerrors.ErrPayloadTooShort } level := binary.LittleEndian.Uint16(data[0:2]) @@ -314,7 +314,7 @@ func (p *StateService) String() string { func (p *StateService) Decode(data []byte) error { if len(data) < 5 { - return models.ErrPayloadTooShort + return lerrors.ErrPayloadTooShort } p.Service = int(data[0]) @@ -357,7 +357,7 @@ func (p *LightState) String() string { func (p *LightState) Decode(data []byte) error { if len(data) < 52 { - return models.ErrPayloadTooShort + return lerrors.ErrPayloadTooShort } hue := binary.LittleEndian.Uint16(data[0:2]) diff --git a/internal/drivers/lifx/state.go b/internal/drivers/lifx/state.go index 458ee82..f28e86b 100644 --- a/internal/drivers/lifx/state.go +++ b/internal/drivers/lifx/state.go @@ -1,6 +1,7 @@ package lifx import ( + "git.aiterp.net/lucifer/new-server/internal/color" "git.aiterp.net/lucifer/new-server/models" "math" "time" @@ -38,7 +39,7 @@ func (s *State) generateUpdate() []Payload { c, ok := s.deviceState.Color.ToHSK() if !ok { - c, _ = models.ParseColorValue("hsk:0,0,4000") + c, _ = color.Parse("hsk:0,0,4000") } l := s.lightState @@ -76,16 +77,16 @@ func (s *State) handleAck(seq uint8) { prevLabel = s.lightState.Label } - color, ok := s.deviceState.Color.ToHSK() + c, ok := s.deviceState.Color.ToHSK() if !ok { - color, _ = models.ParseColorValue("hsk:0,0,4000") + c, _ = color.Parse("hsk:0,0,4000") } s.lightState = &LightState{ - Hue: color.HS.Hue, - Sat: color.HS.Sat, + Hue: c.HS.Hue, + Sat: c.HS.Sat, Bri: s.deviceState.Intensity, - Kelvin: *color.K, + Kelvin: *c.K, On: s.deviceState.Power, Label: prevLabel, } diff --git a/internal/drivers/mill/bridge.go b/internal/drivers/mill/bridge.go index 46033e1..d5982ad 100644 --- a/internal/drivers/mill/bridge.go +++ b/internal/drivers/mill/bridge.go @@ -7,6 +7,7 @@ import ( "crypto/sha1" "encoding/json" "fmt" + "git.aiterp.net/lucifer/new-server/internal/lerrors" "git.aiterp.net/lucifer/new-server/models" "io" "log" @@ -163,9 +164,9 @@ func (b *bridge) command(ctx context.Context, command string, payload interface{ res, err := http.DefaultClient.Do(req) if err != nil { - return models.ErrCannotForwardRequest + return lerrors.ErrCannotForwardRequest } else if res.StatusCode != 200 { - return models.ErrIncorrectToken + return lerrors.ErrIncorrectToken } if target == nil { @@ -174,7 +175,7 @@ func (b *bridge) command(ctx context.Context, command string, payload interface{ err = json.NewDecoder(res.Body).Decode(&target) if err != nil { - return models.ErrUnexpectedResponse + return lerrors.ErrUnexpectedResponse } return nil @@ -190,27 +191,27 @@ func (b *bridge) authenticate(ctx context.Context) error { Password: b.password, }) if err != nil { - return models.ErrMissingToken + return lerrors.ErrMissingToken } - req, err := http.NewRequestWithContext(ctx, "POST", accountEndpoint + "login", bytes.NewReader(body)) + req, err := http.NewRequestWithContext(ctx, "POST", accountEndpoint+"login", bytes.NewReader(body)) if err != nil { - return models.ErrMissingToken + return lerrors.ErrMissingToken } addDefaultHeaders(req) res, err := http.DefaultClient.Do(req) if err != nil { - return models.ErrCannotForwardRequest + return lerrors.ErrCannotForwardRequest } else if res.StatusCode != 200 { - return models.ErrIncorrectToken + return lerrors.ErrIncorrectToken } var resBody authResBody err = json.NewDecoder(res.Body).Decode(&resBody) if err != nil { - return models.ErrBridgeSearchFailed + return lerrors.ErrBridgeSearchFailed } log.Printf("Mill: Authenticated as %s", resBody.NickName) diff --git a/internal/drivers/nanoleaf/bridge.go b/internal/drivers/nanoleaf/bridge.go index 60d0fab..64b827b 100644 --- a/internal/drivers/nanoleaf/bridge.go +++ b/internal/drivers/nanoleaf/bridge.go @@ -5,6 +5,8 @@ import ( "context" "encoding/json" "fmt" + "git.aiterp.net/lucifer/new-server/internal/color" + "git.aiterp.net/lucifer/new-server/internal/lerrors" "git.aiterp.net/lucifer/new-server/models" "github.com/lucasb-eyer/go-colorful" "io" @@ -70,7 +72,7 @@ func (b *bridge) Devices() []models.Device { UserProperties: nil, State: models.DeviceState{ Power: panel.ColorRGBA[3] == 0, - Color: models.ColorValue{RGB: &models.ColorRGB{ + Color: color.Color{RGB: &color.RGB{ Red: rgb.R, Green: rgb.G, Blue: rgb.B, @@ -140,9 +142,9 @@ func (b *bridge) Overview(ctx context.Context) (*Overview, error) { switch res.StatusCode { case 400, 403, 500, 503: - return nil, models.ErrUnexpectedResponse + return nil, lerrors.ErrUnexpectedResponse case 401: - return nil, models.ErrIncorrectToken + return nil, lerrors.ErrIncorrectToken } overview := Overview{} @@ -333,7 +335,7 @@ func (b *bridge) updateEffect(ctx context.Context) error { defer res.Body.Close() if res.StatusCode != 204 { - return models.ErrUnexpectedResponse + return lerrors.ErrUnexpectedResponse } b.mu.Lock() diff --git a/internal/drivers/nanoleaf/driver.go b/internal/drivers/nanoleaf/driver.go index e4a2be5..f86a39b 100644 --- a/internal/drivers/nanoleaf/driver.go +++ b/internal/drivers/nanoleaf/driver.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "git.aiterp.net/lucifer/new-server/internal/lerrors" "git.aiterp.net/lucifer/new-server/models" "net/http" "sync" @@ -30,7 +31,7 @@ func (d *Driver) SearchBridge(ctx context.Context, address, _ string, dryRun boo return nil, err } if deviceInfo.ModelNumber == "" { - return nil, models.ErrUnexpectedResponse + return nil, lerrors.ErrUnexpectedResponse } token := "" @@ -47,7 +48,7 @@ func (d *Driver) SearchBridge(ctx context.Context, address, _ string, dryRun boo defer res.Body.Close() if res.StatusCode != 200 { - return nil, models.ErrBridgeSearchFailed + return nil, lerrors.ErrBridgeSearchFailed } tokenResponse := TokenResponse{} diff --git a/internal/drivers/provider.go b/internal/drivers/provider.go index c3763b4..65626fc 100644 --- a/internal/drivers/provider.go +++ b/internal/drivers/provider.go @@ -1,14 +1,16 @@ package drivers -import "git.aiterp.net/lucifer/new-server/models" +import ( + "git.aiterp.net/lucifer/new-server/internal/lerrors" + "git.aiterp.net/lucifer/new-server/models" +) type DriverMap map[models.DriverKind]models.Driver func (m DriverMap) Provide(kind models.DriverKind) (models.Driver, error) { if m[kind] == nil { - return nil, models.ErrUnknownDriver + return nil, lerrors.ErrUnknownDriver } return m[kind], nil } - diff --git a/models/errors.go b/internal/lerrors/errors.go similarity index 99% rename from models/errors.go rename to internal/lerrors/errors.go index ff337ba..249e9da 100644 --- a/models/errors.go +++ b/internal/lerrors/errors.go @@ -1,4 +1,4 @@ -package models +package lerrors import "errors" diff --git a/internal/mysql/devicerepo.go b/internal/mysql/devicerepo.go index db1a397..bcddc80 100644 --- a/internal/mysql/devicerepo.go +++ b/internal/mysql/devicerepo.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "encoding/json" + "git.aiterp.net/lucifer/new-server/internal/color" "git.aiterp.net/lucifer/new-server/models" sq "github.com/Masterminds/squirrel" "github.com/jmoiron/sqlx" @@ -375,7 +376,7 @@ func (r *DeviceRepo) populate(ctx context.Context, records []deviceRecord) ([]mo for _, state := range states { if state.DeviceID == record.ID { - color, _ := models.ParseColorValue(state.Color) + color, _ := color.Parse(state.Color) device.State = models.DeviceState{ Power: state.Power, diff --git a/internal/mysql/presetrepo.go b/internal/mysql/presetrepo.go index 221307f..c63a6e6 100644 --- a/internal/mysql/presetrepo.go +++ b/internal/mysql/presetrepo.go @@ -2,6 +2,7 @@ package mysql import ( "context" + "git.aiterp.net/lucifer/new-server/internal/color" "git.aiterp.net/lucifer/new-server/models" "github.com/jmoiron/sqlx" ) @@ -85,7 +86,7 @@ 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) + color, _ := color.Parse(record.Value) newList[i] = models.ColorPreset{ ID: record.ID, diff --git a/internal/mysql/util.go b/internal/mysql/util.go index 2dddaa3..4f87d3c 100644 --- a/internal/mysql/util.go +++ b/internal/mysql/util.go @@ -2,16 +2,16 @@ package mysql import ( "database/sql" - "git.aiterp.net/lucifer/new-server/models" + "git.aiterp.net/lucifer/new-server/internal/lerrors" "log" ) func dbErr(err error) error { if err == sql.ErrNoRows { - return models.ErrNotFound + return lerrors.ErrNotFound } else if err != nil { log.Printf("Internal error: %s", err.Error()) - return models.ErrInternal + return lerrors.ErrInternal } return nil diff --git a/models/colorpreset.go b/models/colorpreset.go index c2b3f71..15887ee 100644 --- a/models/colorpreset.go +++ b/models/colorpreset.go @@ -2,13 +2,14 @@ package models import ( "context" + "git.aiterp.net/lucifer/new-server/internal/color" "strings" ) type ColorPreset struct { - ID int `json:"id"` - Name string `json:"name"` - Value ColorValue `json:"value"` + ID int `json:"id"` + Name string `json:"name"` + Value color.Color `json:"value"` } type ColorPresetRepository interface { diff --git a/models/colorvalue.go b/models/colorvalue.go deleted file mode 100644 index c2eda95..0000000 --- a/models/colorvalue.go +++ /dev/null @@ -1,368 +0,0 @@ -package models - -import ( - "fmt" - "github.com/lucasb-eyer/go-colorful" - "strconv" - "strings" -) - -type ColorValue struct { - RGB *ColorRGB `json:"rgb,omitempty"` - HS *ColorHS `json:"hs,omitempty"` - K *int `json:"k,omitempty"` - XY *ColorXY `json:"xy,omitempty"` -} - -func (cv *ColorValue) IsHueSat() bool { - return cv.HS != nil -} - -func (cv *ColorValue) IsHueSatKelvin() bool { - return cv.HS != nil && cv.K != nil -} - -func (cv *ColorValue) IsKelvin() bool { - return cv.K != nil -} - -func (cv *ColorValue) IsEmpty() bool { - return *cv == ColorValue{} -} - -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 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 (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 -} - -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 { - err = ErrBadInput - return - } - - switch tokens[0] { - case "kelvin", "k": - { - parsedPart, err := strconv.Atoi(tokens[1]) - if err != nil { - err = ErrBadInput - break - } - - cv2.K = &parsedPart - } - - case "xy": - { - parts := strings.Split(tokens[1], ",") - if len(parts) < 2 { - err = ErrUnknownColorFormat - return - } - - x, err1 := strconv.ParseFloat(parts[0], 64) - y, err2 := strconv.ParseFloat(parts[1], 64) - if err1 != nil || err2 != nil { - err = ErrBadInput - break - } - - cv2.XY = &ColorXY{x, y} - } - - case "hs": - { - parts := strings.Split(tokens[1], ",") - if len(parts) < 2 { - err = ErrUnknownColorFormat - return - } - - part1, err1 := strconv.ParseFloat(parts[0], 64) - part2, err2 := strconv.ParseFloat(parts[1], 64) - if err1 != nil || err2 != nil { - err = ErrBadInput - break - } - - cv2.HS = &ColorHS{Hue: part1, Sat: part2} - } - - case "hsk": - { - 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.Atoi(parts[2]) - if err1 != nil || err2 != nil || err3 != nil { - 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} - } - - 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 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/device.go b/models/device.go index c171b38..5c8cd9b 100644 --- a/models/device.go +++ b/models/device.go @@ -2,6 +2,8 @@ package models import ( "context" + "git.aiterp.net/lucifer/new-server/internal/color" + "git.aiterp.net/lucifer/new-server/internal/lerrors" "strings" "time" ) @@ -34,10 +36,10 @@ type DeviceUpdate struct { // - Intensity: e.g. brightness, range 0..=1 // - Temperature: e.g. for thermostats type DeviceState struct { - Power bool `json:"power"` - Color ColorValue `json:"color,omitempty"` - Intensity float64 `json:"intensity,omitempty"` - Temperature int `json:"temperature"` + Power bool `json:"power"` + Color color.Color `json:"color,omitempty"` + Intensity float64 `json:"intensity,omitempty"` + Temperature int `json:"temperature"` } type DeviceScene struct { @@ -83,11 +85,11 @@ func DeviceCapabilitiesToStrings(caps []DeviceCapability) []string { var ( DCPower DeviceCapability = "Power" - DCColorHS DeviceCapability = "ColorHS" + DCColorHS DeviceCapability = "HueSat" DCColorHSK DeviceCapability = "ColorHSK" DCColorKelvin DeviceCapability = "ColorKelvin" - DCColorXY DeviceCapability = "ColorXY" - DCColorRGB DeviceCapability = "ColorRGB" + DCColorXY DeviceCapability = "XY" + DCColorRGB DeviceCapability = "RGB" DCButtons DeviceCapability = "Buttons" DCPresence DeviceCapability = "Presence" DCIntensity DeviceCapability = "Intensity" @@ -128,7 +130,7 @@ func (d *Device) ApplyUpdate(update DeviceUpdate) { func (d *Device) Validate() error { d.Name = strings.Trim(d.Name, " \t\n ") if d.Name == "" { - return ErrInvalidName + return lerrors.ErrInvalidName } newCaps := make([]DeviceCapability, 0, len(d.Capabilities)) @@ -186,7 +188,7 @@ func (d *Device) SetState(newState NewDeviceState) error { } if newState.Color != nil { - parsed, err := ParseColorValue(*newState.Color) + parsed, err := color.Parse(*newState.Color) if err != nil { return err } @@ -196,7 +198,7 @@ func (d *Device) SetState(newState NewDeviceState) error { 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.HS = &color.HueSat{Hue: 0, Sat: 0} } d.State.Color = parsed @@ -237,8 +239,8 @@ func (s *NewDeviceState) Interpolate(other NewDeviceState, fac float64) NewDevic } if s.Color != nil && other.Color != nil { - sc, err := ParseColorValue(*s.Color) - oc, err2 := ParseColorValue(*other.Color) + sc, err := color.Parse(*s.Color) + oc, err2 := color.Parse(*other.Color) if err == nil && err2 == nil { rc := sc.Interpolate(oc, fac) rcStr := rc.String() diff --git a/models/scene.go b/models/scene.go index 63d1dba..18cbe1b 100644 --- a/models/scene.go +++ b/models/scene.go @@ -2,6 +2,7 @@ package models import ( "context" + "git.aiterp.net/lucifer/new-server/internal/lerrors" "math" "math/rand" "sort" @@ -18,10 +19,10 @@ type Scene struct { func (s *Scene) Validate() error { if s.IntervalMS < 0 { - return ErrSceneInvalidInterval + return lerrors.ErrSceneInvalidInterval } if len(s.Roles) == 0 { - return ErrSceneNoRoles + return lerrors.ErrSceneNoRoles } for _, role := range s.Roles { @@ -118,31 +119,31 @@ func (d *SceneRunContext) IntervalFac() float64 { func (r *SceneRole) Validate() error { if len(r.States) == 0 { - return ErrSceneRoleNoStates + return lerrors.ErrSceneRoleNoStates } switch r.TargetKind { case RKTag, RKBridgeID, RKDeviceID, RKName, RKAll: default: - return ErrBadInput + return lerrors.ErrBadInput } switch r.PowerMode { case SPScene, SPDevice, SPBoth: default: - return ErrSceneRoleUnknownPowerMode + return lerrors.ErrSceneRoleUnknownPowerMode } switch r.Effect { case SEStatic, SERandom, SEGradient, SEWalkingGradient, SETransition, SEMotion, SETemperature: default: - return ErrSceneRoleUnknownEffect + return lerrors.ErrSceneRoleUnknownEffect } switch r.Order { case "", "-name", "name", "+name", "-id", "id", "+id": default: - return ErrSceneRoleUnsupportedOrdering + return lerrors.ErrSceneRoleUnsupportedOrdering } return nil