Browse Source

refactor color system to be separate from models, also move errors out of models.

feature-colorvalue2
Gisle Aune 2 years ago
parent
commit
62e7cb88f4
  1. 3
      app/api/devices.go
  2. 7
      app/api/presets.go
  3. 28
      app/api/util.go
  4. 3
      cmd/bridgetest/main.go
  5. 10
      cmd/xy/main.go
  6. 369
      internal/color/color.go
  7. 10
      internal/color/hs.go
  8. 16
      internal/color/rgb.go
  9. 50
      internal/color/xy.go
  10. 5
      internal/drivers/hue/bridge.go
  11. 3
      internal/drivers/hue/driver.go
  12. 4
      internal/drivers/hue2/bridge.go
  13. 4
      internal/drivers/hue2/client.go
  14. 10
      internal/drivers/hue2/data.go
  15. 3
      internal/drivers/hue2/driver.go
  16. 12
      internal/drivers/lifx/bridge.go
  17. 6
      internal/drivers/lifx/client.go
  18. 6
      internal/drivers/lifx/packet.go
  19. 14
      internal/drivers/lifx/payloads.go
  20. 13
      internal/drivers/lifx/state.go
  21. 19
      internal/drivers/mill/bridge.go
  22. 10
      internal/drivers/nanoleaf/bridge.go
  23. 5
      internal/drivers/nanoleaf/driver.go
  24. 8
      internal/drivers/provider.go
  25. 2
      internal/lerrors/errors.go
  26. 3
      internal/mysql/devicerepo.go
  27. 3
      internal/mysql/presetrepo.go
  28. 6
      internal/mysql/util.go
  29. 7
      models/colorpreset.go
  30. 368
      models/colorvalue.go
  31. 26
      models/device.go
  32. 15
      models/scene.go

3
app/api/devices.go

@ -4,6 +4,7 @@ import (
"context" "context"
"git.aiterp.net/lucifer/new-server/app/config" "git.aiterp.net/lucifer/new-server/app/config"
"git.aiterp.net/lucifer/new-server/app/services/publisher" "git.aiterp.net/lucifer/new-server/app/services/publisher"
"git.aiterp.net/lucifer/new-server/internal/lerrors"
"git.aiterp.net/lucifer/new-server/models" "git.aiterp.net/lucifer/new-server/models"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"log" "log"
@ -305,7 +306,7 @@ func Devices(r gin.IRoutes) {
forgettableDriver, ok := driver.(models.ForgettableDriver) forgettableDriver, ok := driver.(models.ForgettableDriver)
if !ok { if !ok {
return nil, models.ErrCannotForget
return nil, lerrors.ErrCannotForget
} }
err = forgettableDriver.ForgetDevice(ctxOf(c), bridge, *device) err = forgettableDriver.ForgetDevice(ctxOf(c), bridge, *device)
if err != nil { if err != nil {

7
app/api/presets.go

@ -2,6 +2,7 @@ package api
import ( import (
"git.aiterp.net/lucifer/new-server/app/config" "git.aiterp.net/lucifer/new-server/app/config"
"git.aiterp.net/lucifer/new-server/internal/color"
"git.aiterp.net/lucifer/new-server/models" "git.aiterp.net/lucifer/new-server/models"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -25,13 +26,13 @@ func ColorPresets(r gin.IRoutes) {
return nil, err return nil, err
} }
newColor, err := models.ParseColorValue(body.ColorString)
newColor, err := color.Parse(body.ColorString)
if err != nil { if err != nil {
return nil, err return nil, err
} }
preset := models.ColorPreset{ preset := models.ColorPreset{
Name: body.Name,
Name: body.Name,
Value: newColor, Value: newColor,
} }
@ -64,7 +65,7 @@ func ColorPresets(r gin.IRoutes) {
} }
if body.ColorString != nil { if body.ColorString != nil {
newColor, err := models.ParseColorValue(*body.ColorString)
newColor, err := color.Parse(*body.ColorString)
if err != nil { if err != nil {
return nil, err return nil, err
} }

28
app/api/util.go

@ -3,25 +3,25 @@ package api
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"git.aiterp.net/lucifer/new-server/models"
"git.aiterp.net/lucifer/new-server/internal/lerrors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"strconv" "strconv"
) )
var errorMap = map[error]int{ 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 { type response struct {
@ -66,7 +66,7 @@ func intParam(c *gin.Context, key string) int {
func parseBody(c *gin.Context, target interface{}) error { func parseBody(c *gin.Context, target interface{}) error {
err := json.NewDecoder(c.Request.Body).Decode(target) err := json.NewDecoder(c.Request.Body).Decode(target)
if err != nil { if err != nil {
return models.ErrBadInput
return lerrors.ErrBadInput
} }
return nil return nil

3
cmd/bridgetest/main.go

@ -7,6 +7,7 @@ import (
"flag" "flag"
"fmt" "fmt"
"git.aiterp.net/lucifer/new-server/app/config" "git.aiterp.net/lucifer/new-server/app/config"
"git.aiterp.net/lucifer/new-server/internal/color"
"git.aiterp.net/lucifer/new-server/models" "git.aiterp.net/lucifer/new-server/models"
"log" "log"
"os" "os"
@ -124,7 +125,7 @@ func main() {
continue continue
} }
color, err := models.ParseColorValue(tokens[2])
color, err := color.Parse(tokens[2])
if err != nil { if err != nil {
_, _ = fmt.Fprintln(os.Stderr, "Invalid color:", err) _, _ = fmt.Fprintln(os.Stderr, "Invalid color:", err)
continue continue

10
cmd/xy/main.go

@ -1,21 +1,15 @@
package main package main
import ( import (
"encoding/json"
"git.aiterp.net/lucifer/new-server/models"
"git.aiterp.net/lucifer/new-server/internal/color"
"log" "log"
) )
func main() { func main() {
cv, _ := models.ParseColorValue("rgb:#fff")
cv, _ := color.Parse("xy:0.1944,0.0942")
hs, _ := cv.ToHS() hs, _ := cv.ToHS()
log.Println(cv.String()) log.Println(cv.String())
log.Println(hs.String()) log.Println(hs.String())
xy, _ := hs.ToXY() xy, _ := hs.ToXY()
log.Println(xy.String()) log.Println(xy.String())
} }
func toJSON(v interface{}) string {
j, _ := json.Marshal(v)
return string(j)
}

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

10
models/colorhs.go → internal/color/hs.go

@ -1,17 +1,17 @@
package models
package color
import "github.com/lucasb-eyer/go-colorful" import "github.com/lucasb-eyer/go-colorful"
type ColorHS struct {
type HueSat struct {
Hue float64 `json:"hue"` Hue float64 `json:"hue"`
Sat float64 `json:"sat"` Sat float64 `json:"sat"`
} }
func (hs ColorHS) ToXY() ColorXY {
func (hs HueSat) ToXY() XY {
return hs.ToRGB().ToXY() return hs.ToRGB().ToXY()
} }
func (hs ColorHS) ToRGB() ColorRGB {
func (hs HueSat) ToRGB() RGB {
c := colorful.Hsv(hs.Hue, hs.Sat, 1) 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}
} }

16
models/colorrgb.go → internal/color/rgb.go

@ -1,28 +1,28 @@
package models
package color
import "github.com/lucasb-eyer/go-colorful" import "github.com/lucasb-eyer/go-colorful"
type ColorRGB struct {
type RGB struct {
Red float64 `json:"red"` Red float64 `json:"red"`
Green float64 `json:"green"` Green float64 `json:"green"`
Blue float64 `json:"blue"` 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() hue, sat, _ := colorful.Color{R: rgb.Red, G: rgb.Green, B: rgb.Blue}.Hsv()
hsv2 := colorful.Hsv(hue, sat, intensity) 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() 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() x, y, z := (colorful.Color{R: rgb.Red, G: rgb.Green, B: rgb.Blue}).Xyz()
return ColorXY{
return XY{
X: x / (x + y + z), X: x / (x + y + z),
Y: y / (x + y + z), Y: y / (x + y + z),
} }

50
models/colorxy.go → internal/color/xy.go

@ -1,4 +1,4 @@
package models
package color
import ( import (
"github.com/lucasb-eyer/go-colorful" "github.com/lucasb-eyer/go-colorful"
@ -8,17 +8,17 @@ import (
const eps = 0.0001 const eps = 0.0001
const epsSquare = eps * eps 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) 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 x, y := color.X, color.Y
x1, y1 := cg.Red.X, cg.Red.Y x1, y1 := cg.Red.X, cg.Red.Y
x2, y2 := cg.Green.X, cg.Green.Y x2, y2 := cg.Green.X, cg.Green.Y
@ -31,7 +31,7 @@ func (cg *ColorGamut) naiveContains(color ColorXY) bool {
return checkSide1 && checkSide2 && checkSide3 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 x1, y1 := cg.Red.X, cg.Red.Y
x2, y2 := cg.Green.X, cg.Green.Y x2, y2 := cg.Green.X, cg.Green.Y
x3, y3 := cg.Blue.X, cg.Blue.Y x3, y3 := cg.Blue.X, cg.Blue.Y
@ -44,14 +44,14 @@ func (cg *ColorGamut) getBounds() (xMin, xMax, yMin, yMax float64) {
return return
} }
func (cg *ColorGamut) isInBounds(color ColorXY) bool {
func (cg *Gamut) isInBounds(color XY) bool {
x, y := color.X, color.Y x, y := color.X, color.Y
xMin, xMax, yMin, yMax := cg.getBounds() xMin, xMax, yMin, yMax := cg.getBounds()
return !(x < xMin || xMax < x || y < yMin || yMax < y) 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) sqLength1 := (x2-x1)*(x2-x1) + (y2-y1)*(y2-y1)
dotProduct := ((x-x1)*(x2-x1) + (y-y1)*(y2-y1)) / sqLength1 dotProduct := ((x-x1)*(x2-x1) + (y-y1)*(y2-y1)) / sqLength1
if dotProduct < 0 { 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 x, y := color.X, color.Y
x1, y1 := cg.Red.X, cg.Red.Y x1, y1 := cg.Red.X, cg.Red.Y
x2, y2 := cg.Green.X, cg.Green.Y x2, y2 := cg.Green.X, cg.Green.Y
@ -83,7 +83,7 @@ func (cg *ColorGamut) atTheEdge(color ColorXY) bool {
return false return false
} }
func (cg *ColorGamut) Contains(color ColorXY) bool {
func (cg *Gamut) Contains(color XY) bool {
if cg == nil { if cg == nil {
return true return true
} }
@ -91,18 +91,18 @@ func (cg *ColorGamut) Contains(color ColorXY) bool {
return cg.isInBounds(color) && (cg.naiveContains(color) || cg.atTheEdge(color)) 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) { if cg.Contains(color) {
return color return color
} }
var best *ColorXY
var best *XY
xMin, xMax, yMin, yMax := cg.getBounds() xMin, xMax, yMin, yMax := cg.getBounds()
for x := xMin; x < xMax; x += 0.001 { for x := xMin; x < xMax; x += 0.001 {
for y := yMin; y < yMax; y += 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 cg.Contains(color2) {
if best == nil || color.DistanceTo(color2) < color.DistanceTo(*best) { 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 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 { 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 cg.atTheEdge(color2) {
if best == nil || color.DistanceTo(color2) < color.DistanceTo(*best) { 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 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 { 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 cg.atTheEdge(color2) {
if best == nil || color.DistanceTo(color2) < color.DistanceTo(*best) { if best == nil || color.DistanceTo(color2) < color.DistanceTo(*best) {
@ -154,28 +154,28 @@ func (cg *ColorGamut) Conform(color ColorXY) ColorXY {
return *best return *best
} }
type ColorXY struct {
type XY struct {
X float64 `json:"x"` X float64 `json:"x"`
Y float64 `json:"y"` 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() h, s, _ := colorful.Xyy(xy.X, xy.Y, 0.5).Hsv()
c := colorful.Hsv(h, s, 1) 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() 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)) 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, X: math.Round(xy.X*10000) / 10000,
Y: math.Round(xy.Y*10000) / 10000, Y: math.Round(xy.Y*10000) / 10000,
} }

5
internal/drivers/hue/bridge.go

@ -5,6 +5,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"git.aiterp.net/lucifer/new-server/internal/lerrors"
"git.aiterp.net/lucifer/new-server/models" "git.aiterp.net/lucifer/new-server/models"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"io" "io"
@ -159,7 +160,7 @@ func (b *Bridge) ForgetDevice(ctx context.Context, device models.Device) error {
} }
b.mu.Unlock() b.mu.Unlock()
if !found { if !found {
return models.ErrNotFound
return lerrors.ErrNotFound
} }
// Delete light from bridge // Delete light from bridge
@ -237,7 +238,7 @@ func (b *Bridge) getToken(ctx context.Context) (string, error) {
return "", errLinkButtonNotPressed return "", errLinkButtonNotPressed
} }
if result[0].Success == nil { if result[0].Success == nil {
return "", models.ErrUnexpectedResponse
return "", lerrors.ErrUnexpectedResponse
} }
return result[0].Success.Username, nil return result[0].Success.Username, nil

3
internal/drivers/hue/driver.go

@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"git.aiterp.net/lucifer/new-server/internal/lerrors"
"git.aiterp.net/lucifer/new-server/models" "git.aiterp.net/lucifer/new-server/models"
"log" "log"
"net/http" "net/http"
@ -22,7 +23,7 @@ type Driver struct {
func (d *Driver) SearchBridge(ctx context.Context, address, _ string, dryRun bool) ([]models.Bridge, error) { func (d *Driver) SearchBridge(ctx context.Context, address, _ string, dryRun bool) ([]models.Bridge, error) {
if address == "" { if address == "" {
if !dryRun { if !dryRun {
return nil, models.ErrAddressOnlyDryRunnable
return nil, lerrors.ErrAddressOnlyDryRunnable
} }
res, err := http.Get("https://discovery.meethue.com") res, err := http.Get("https://discovery.meethue.com")

4
internal/drivers/hue2/bridge.go

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"git.aiterp.net/lucifer/new-server/internal/color"
"git.aiterp.net/lucifer/new-server/models" "git.aiterp.net/lucifer/new-server/models"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"log" "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 { } else if xyColor, ok := device.State.Color.ToXY(); ok && light.Color != nil {
xy := light.Color.Gamut.Conform(*xyColor.XY).Round() xy := light.Color.Gamut.Conform(*xyColor.XY).Round()
if xy.DistanceTo(light.Color.XY) > 0.0009 || (light.ColorTemperature != nil && light.ColorTemperature.Mirek != nil) { 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 update.ColorXY = &xy
changed = true changed = true
} }
@ -428,7 +428,7 @@ func (b *Bridge) GenerateDevices() []models.Device {
} }
if light.Color != nil { if light.Color != nil {
if device.State.Color.IsEmpty() { if device.State.Color.IsEmpty() {
device.State.Color = models.ColorValue{
device.State.Color = color.Color{
XY: &light.Color.XY, XY: &light.Color.XY,
} }
} }

4
internal/drivers/hue2/client.go

@ -8,7 +8,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"git.aiterp.net/lucifer/new-server/models"
"git.aiterp.net/lucifer/new-server/internal/lerrors"
"io" "io"
"log" "log"
"net" "net"
@ -52,7 +52,7 @@ func (c *Client) Register(ctx context.Context) (string, error) {
} }
} }
if result[0].Success == nil { if result[0].Success == nil {
return "", models.ErrUnexpectedResponse
return "", lerrors.ErrUnexpectedResponse
} }
c.token = result[0].Success.Username c.token = result[0].Success.Username

10
internal/drivers/hue2/data.go

@ -4,7 +4,7 @@ import (
"encoding/json" "encoding/json"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"git.aiterp.net/lucifer/new-server/models"
"git.aiterp.net/lucifer/new-server/internal/color"
"strings" "strings"
"time" "time"
) )
@ -152,9 +152,9 @@ type LightDimming struct {
} }
type LightColor 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 { type LightCT struct {
@ -182,7 +182,7 @@ type LightAlert struct {
type ResourceUpdate struct { type ResourceUpdate struct {
Name *string Name *string
Power *bool Power *bool
ColorXY *models.ColorXY
ColorXY *color.XY
Brightness *float64 Brightness *float64
Mirek *int Mirek *int
TransitionDuration *time.Duration TransitionDuration *time.Duration

3
internal/drivers/hue2/driver.go

@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"git.aiterp.net/lucifer/new-server/internal/lerrors"
"git.aiterp.net/lucifer/new-server/models" "git.aiterp.net/lucifer/new-server/models"
"net/http" "net/http"
"sync" "sync"
@ -19,7 +20,7 @@ type Driver struct {
func (d *Driver) SearchBridge(ctx context.Context, address, token string, dryRun bool) ([]models.Bridge, error) { func (d *Driver) SearchBridge(ctx context.Context, address, token string, dryRun bool) ([]models.Bridge, error) {
if address == "" { if address == "" {
if !dryRun { if !dryRun {
return nil, models.ErrAddressOnlyDryRunnable
return nil, lerrors.ErrAddressOnlyDryRunnable
} }
res, err := http.Get("https://discovery.meethue.com") res, err := http.Get("https://discovery.meethue.com")

12
internal/drivers/lifx/bridge.go

@ -2,6 +2,8 @@ package lifx
import ( import (
"context" "context"
"git.aiterp.net/lucifer/new-server/internal/color"
"git.aiterp.net/lucifer/new-server/internal/lerrors"
"git.aiterp.net/lucifer/new-server/models" "git.aiterp.net/lucifer/new-server/models"
"log" "log"
"sync" "sync"
@ -20,7 +22,7 @@ type Bridge struct {
func (b *Bridge) StartSearch(ctx context.Context) error { func (b *Bridge) StartSearch(ctx context.Context) error {
c := b.getClient() c := b.getClient()
if c == nil { if c == nil {
return models.ErrBridgeRunningRequired
return lerrors.ErrBridgeRunningRequired
} }
_, err := c.HorribleBroadcast(ctx, &GetService{}) _, err := c.HorribleBroadcast(ctx, &GetService{})
@ -129,9 +131,9 @@ func (b *Bridge) Run(ctx context.Context, debug bool) error {
for { for {
target, seq, payload, err := client.Recv(time.Millisecond * 200) 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) 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 { if ctx.Err() != nil {
return ctx.Err() return ctx.Err()
} }
@ -168,8 +170,8 @@ func (b *Bridge) Run(ctx context.Context, debug bool) error {
if state.deviceState == nil { if state.deviceState == nil {
state.deviceState = &models.DeviceState{ state.deviceState = &models.DeviceState{
Power: p.On, Power: p.On,
Color: models.ColorValue{
HS: &models.ColorHS{
Color: color.Color{
HS: &color.HueSat{
Hue: p.Hue, Hue: p.Hue,
Sat: p.Sat, Sat: p.Sat,
}, },

6
internal/drivers/lifx/client.go

@ -4,7 +4,7 @@ import (
"context" "context"
"encoding/binary" "encoding/binary"
"errors" "errors"
"git.aiterp.net/lucifer/new-server/models"
"git.aiterp.net/lucifer/new-server/internal/lerrors"
"log" "log"
"math/rand" "math/rand"
"net" "net"
@ -215,7 +215,7 @@ func (c *Client) Recv(timeout time.Duration) (target string, seq uint8, payload
} }
if err != nil { if err != nil {
if netErr, ok := err.(*net.OpError); ok && netErr.Timeout() { if netErr, ok := err.(*net.OpError); ok && netErr.Timeout() {
err = models.ErrReadTimeout
err = lerrors.ErrReadTimeout
return return
} }
@ -224,7 +224,7 @@ func (c *Client) Recv(timeout time.Duration) (target string, seq uint8, payload
packet := Packet(c.buf[:n]) packet := Packet(c.buf[:n])
if n < 2 || packet.Size() != n && packet.Protocol() != 1024 { if n < 2 || packet.Size() != n && packet.Protocol() != 1024 {
err = models.ErrInvalidAddress
err = lerrors.ErrInvalidAddress
return return
} }

6
internal/drivers/lifx/packet.go

@ -3,7 +3,7 @@ package lifx
import ( import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"git.aiterp.net/lucifer/new-server/models"
"git.aiterp.net/lucifer/new-server/internal/lerrors"
"log" "log"
"net" "net"
) )
@ -60,7 +60,7 @@ func (p *Packet) SetTarget(v string) error {
return err return err
} }
if len(addr) != 6 { if len(addr) != 6 {
return models.ErrInvalidAddress
return lerrors.ErrInvalidAddress
} }
copy((*p)[8:], addr) copy((*p)[8:], addr)
@ -133,7 +133,7 @@ func (p *Packet) Payload() (res Payload, err error) {
res = &SetLightPower{} res = &SetLightPower{}
err = res.Decode((*p)[36:]) err = res.Decode((*p)[36:])
default: default:
err = models.ErrUnrecognizedPacketType
err = lerrors.ErrUnrecognizedPacketType
} }
if err != nil { if err != nil {

14
internal/drivers/lifx/payloads.go

@ -3,7 +3,7 @@ package lifx
import ( import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"git.aiterp.net/lucifer/new-server/models"
"git.aiterp.net/lucifer/new-server/internal/lerrors"
"math" "math"
"time" "time"
) )
@ -82,7 +82,7 @@ type StateHostFirmware struct {
func (p *StateHostFirmware) Decode(data []byte) error { func (p *StateHostFirmware) Decode(data []byte) error {
if len(data) < 20 { if len(data) < 20 {
return models.ErrPayloadTooShort
return lerrors.ErrPayloadTooShort
} }
ts := int64(binary.LittleEndian.Uint64(data[0:8])) ts := int64(binary.LittleEndian.Uint64(data[0:8]))
@ -123,7 +123,7 @@ type StateVersion struct {
func (p *StateVersion) Decode(data []byte) error { func (p *StateVersion) Decode(data []byte) error {
if len(data) < 8 { if len(data) < 8 {
return models.ErrPayloadTooShort
return lerrors.ErrPayloadTooShort
} }
p.Vendor = binary.LittleEndian.Uint32(data[0:4]) p.Vendor = binary.LittleEndian.Uint32(data[0:4])
@ -207,7 +207,7 @@ type SetColor struct {
func (p *SetColor) Decode(data []byte) error { func (p *SetColor) Decode(data []byte) error {
if len(data) < 13 { if len(data) < 13 {
return models.ErrPayloadTooShort
return lerrors.ErrPayloadTooShort
} }
hue := binary.LittleEndian.Uint16(data[1:3]) hue := binary.LittleEndian.Uint16(data[1:3])
@ -263,7 +263,7 @@ type SetLightPower struct {
func (p *SetLightPower) Decode(data []byte) error { func (p *SetLightPower) Decode(data []byte) error {
if len(data) < 6 { if len(data) < 6 {
return models.ErrPayloadTooShort
return lerrors.ErrPayloadTooShort
} }
level := binary.LittleEndian.Uint16(data[0:2]) level := binary.LittleEndian.Uint16(data[0:2])
@ -314,7 +314,7 @@ func (p *StateService) String() string {
func (p *StateService) Decode(data []byte) error { func (p *StateService) Decode(data []byte) error {
if len(data) < 5 { if len(data) < 5 {
return models.ErrPayloadTooShort
return lerrors.ErrPayloadTooShort
} }
p.Service = int(data[0]) p.Service = int(data[0])
@ -357,7 +357,7 @@ func (p *LightState) String() string {
func (p *LightState) Decode(data []byte) error { func (p *LightState) Decode(data []byte) error {
if len(data) < 52 { if len(data) < 52 {
return models.ErrPayloadTooShort
return lerrors.ErrPayloadTooShort
} }
hue := binary.LittleEndian.Uint16(data[0:2]) hue := binary.LittleEndian.Uint16(data[0:2])

13
internal/drivers/lifx/state.go

@ -1,6 +1,7 @@
package lifx package lifx
import ( import (
"git.aiterp.net/lucifer/new-server/internal/color"
"git.aiterp.net/lucifer/new-server/models" "git.aiterp.net/lucifer/new-server/models"
"math" "math"
"time" "time"
@ -38,7 +39,7 @@ func (s *State) generateUpdate() []Payload {
c, ok := s.deviceState.Color.ToHSK() c, ok := s.deviceState.Color.ToHSK()
if !ok { if !ok {
c, _ = models.ParseColorValue("hsk:0,0,4000")
c, _ = color.Parse("hsk:0,0,4000")
} }
l := s.lightState l := s.lightState
@ -76,16 +77,16 @@ func (s *State) handleAck(seq uint8) {
prevLabel = s.lightState.Label prevLabel = s.lightState.Label
} }
color, ok := s.deviceState.Color.ToHSK()
c, ok := s.deviceState.Color.ToHSK()
if !ok { if !ok {
color, _ = models.ParseColorValue("hsk:0,0,4000")
c, _ = color.Parse("hsk:0,0,4000")
} }
s.lightState = &LightState{ s.lightState = &LightState{
Hue: color.HS.Hue,
Sat: color.HS.Sat,
Hue: c.HS.Hue,
Sat: c.HS.Sat,
Bri: s.deviceState.Intensity, Bri: s.deviceState.Intensity,
Kelvin: *color.K,
Kelvin: *c.K,
On: s.deviceState.Power, On: s.deviceState.Power,
Label: prevLabel, Label: prevLabel,
} }

19
internal/drivers/mill/bridge.go

@ -7,6 +7,7 @@ import (
"crypto/sha1" "crypto/sha1"
"encoding/json" "encoding/json"
"fmt" "fmt"
"git.aiterp.net/lucifer/new-server/internal/lerrors"
"git.aiterp.net/lucifer/new-server/models" "git.aiterp.net/lucifer/new-server/models"
"io" "io"
"log" "log"
@ -163,9 +164,9 @@ func (b *bridge) command(ctx context.Context, command string, payload interface{
res, err := http.DefaultClient.Do(req) res, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return models.ErrCannotForwardRequest
return lerrors.ErrCannotForwardRequest
} else if res.StatusCode != 200 { } else if res.StatusCode != 200 {
return models.ErrIncorrectToken
return lerrors.ErrIncorrectToken
} }
if target == nil { 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) err = json.NewDecoder(res.Body).Decode(&target)
if err != nil { if err != nil {
return models.ErrUnexpectedResponse
return lerrors.ErrUnexpectedResponse
} }
return nil return nil
@ -190,27 +191,27 @@ func (b *bridge) authenticate(ctx context.Context) error {
Password: b.password, Password: b.password,
}) })
if err != nil { 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 { if err != nil {
return models.ErrMissingToken
return lerrors.ErrMissingToken
} }
addDefaultHeaders(req) addDefaultHeaders(req)
res, err := http.DefaultClient.Do(req) res, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return models.ErrCannotForwardRequest
return lerrors.ErrCannotForwardRequest
} else if res.StatusCode != 200 { } else if res.StatusCode != 200 {
return models.ErrIncorrectToken
return lerrors.ErrIncorrectToken
} }
var resBody authResBody var resBody authResBody
err = json.NewDecoder(res.Body).Decode(&resBody) err = json.NewDecoder(res.Body).Decode(&resBody)
if err != nil { if err != nil {
return models.ErrBridgeSearchFailed
return lerrors.ErrBridgeSearchFailed
} }
log.Printf("Mill: Authenticated as %s", resBody.NickName) log.Printf("Mill: Authenticated as %s", resBody.NickName)

10
internal/drivers/nanoleaf/bridge.go

@ -5,6 +5,8 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"git.aiterp.net/lucifer/new-server/internal/color"
"git.aiterp.net/lucifer/new-server/internal/lerrors"
"git.aiterp.net/lucifer/new-server/models" "git.aiterp.net/lucifer/new-server/models"
"github.com/lucasb-eyer/go-colorful" "github.com/lucasb-eyer/go-colorful"
"io" "io"
@ -70,7 +72,7 @@ func (b *bridge) Devices() []models.Device {
UserProperties: nil, UserProperties: nil,
State: models.DeviceState{ State: models.DeviceState{
Power: panel.ColorRGBA[3] == 0, Power: panel.ColorRGBA[3] == 0,
Color: models.ColorValue{RGB: &models.ColorRGB{
Color: color.Color{RGB: &color.RGB{
Red: rgb.R, Red: rgb.R,
Green: rgb.G, Green: rgb.G,
Blue: rgb.B, Blue: rgb.B,
@ -140,9 +142,9 @@ func (b *bridge) Overview(ctx context.Context) (*Overview, error) {
switch res.StatusCode { switch res.StatusCode {
case 400, 403, 500, 503: case 400, 403, 500, 503:
return nil, models.ErrUnexpectedResponse
return nil, lerrors.ErrUnexpectedResponse
case 401: case 401:
return nil, models.ErrIncorrectToken
return nil, lerrors.ErrIncorrectToken
} }
overview := Overview{} overview := Overview{}
@ -333,7 +335,7 @@ func (b *bridge) updateEffect(ctx context.Context) error {
defer res.Body.Close() defer res.Body.Close()
if res.StatusCode != 204 { if res.StatusCode != 204 {
return models.ErrUnexpectedResponse
return lerrors.ErrUnexpectedResponse
} }
b.mu.Lock() b.mu.Lock()

5
internal/drivers/nanoleaf/driver.go

@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"git.aiterp.net/lucifer/new-server/internal/lerrors"
"git.aiterp.net/lucifer/new-server/models" "git.aiterp.net/lucifer/new-server/models"
"net/http" "net/http"
"sync" "sync"
@ -30,7 +31,7 @@ func (d *Driver) SearchBridge(ctx context.Context, address, _ string, dryRun boo
return nil, err return nil, err
} }
if deviceInfo.ModelNumber == "" { if deviceInfo.ModelNumber == "" {
return nil, models.ErrUnexpectedResponse
return nil, lerrors.ErrUnexpectedResponse
} }
token := "" token := ""
@ -47,7 +48,7 @@ func (d *Driver) SearchBridge(ctx context.Context, address, _ string, dryRun boo
defer res.Body.Close() defer res.Body.Close()
if res.StatusCode != 200 { if res.StatusCode != 200 {
return nil, models.ErrBridgeSearchFailed
return nil, lerrors.ErrBridgeSearchFailed
} }
tokenResponse := TokenResponse{} tokenResponse := TokenResponse{}

8
internal/drivers/provider.go

@ -1,14 +1,16 @@
package drivers 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 type DriverMap map[models.DriverKind]models.Driver
func (m DriverMap) Provide(kind models.DriverKind) (models.Driver, error) { func (m DriverMap) Provide(kind models.DriverKind) (models.Driver, error) {
if m[kind] == nil { if m[kind] == nil {
return nil, models.ErrUnknownDriver
return nil, lerrors.ErrUnknownDriver
} }
return m[kind], nil return m[kind], nil
} }

2
models/errors.go → internal/lerrors/errors.go

@ -1,4 +1,4 @@
package models
package lerrors
import "errors" import "errors"

3
internal/mysql/devicerepo.go

@ -4,6 +4,7 @@ import (
"context" "context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"git.aiterp.net/lucifer/new-server/internal/color"
"git.aiterp.net/lucifer/new-server/models" "git.aiterp.net/lucifer/new-server/models"
sq "github.com/Masterminds/squirrel" sq "github.com/Masterminds/squirrel"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
@ -375,7 +376,7 @@ func (r *DeviceRepo) populate(ctx context.Context, records []deviceRecord) ([]mo
for _, state := range states { for _, state := range states {
if state.DeviceID == record.ID { if state.DeviceID == record.ID {
color, _ := models.ParseColorValue(state.Color)
color, _ := color.Parse(state.Color)
device.State = models.DeviceState{ device.State = models.DeviceState{
Power: state.Power, Power: state.Power,

3
internal/mysql/presetrepo.go

@ -2,6 +2,7 @@ package mysql
import ( import (
"context" "context"
"git.aiterp.net/lucifer/new-server/internal/color"
"git.aiterp.net/lucifer/new-server/models" "git.aiterp.net/lucifer/new-server/models"
"github.com/jmoiron/sqlx" "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 { func (c *ColorPresetRepo) fromRecords(records ...presetRecord) []models.ColorPreset {
newList := make([]models.ColorPreset, len(records), len(records)) newList := make([]models.ColorPreset, len(records), len(records))
for i, record := range records { for i, record := range records {
color, _ := models.ParseColorValue(record.Value)
color, _ := color.Parse(record.Value)
newList[i] = models.ColorPreset{ newList[i] = models.ColorPreset{
ID: record.ID, ID: record.ID,

6
internal/mysql/util.go

@ -2,16 +2,16 @@ package mysql
import ( import (
"database/sql" "database/sql"
"git.aiterp.net/lucifer/new-server/models"
"git.aiterp.net/lucifer/new-server/internal/lerrors"
"log" "log"
) )
func dbErr(err error) error { func dbErr(err error) error {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return models.ErrNotFound
return lerrors.ErrNotFound
} else if err != nil { } else if err != nil {
log.Printf("Internal error: %s", err.Error()) log.Printf("Internal error: %s", err.Error())
return models.ErrInternal
return lerrors.ErrInternal
} }
return nil return nil

7
models/colorpreset.go

@ -2,13 +2,14 @@ package models
import ( import (
"context" "context"
"git.aiterp.net/lucifer/new-server/internal/color"
"strings" "strings"
) )
type ColorPreset struct { 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 { type ColorPresetRepository interface {

368
models/colorvalue.go

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

26
models/device.go

@ -2,6 +2,8 @@ package models
import ( import (
"context" "context"
"git.aiterp.net/lucifer/new-server/internal/color"
"git.aiterp.net/lucifer/new-server/internal/lerrors"
"strings" "strings"
"time" "time"
) )
@ -34,10 +36,10 @@ type DeviceUpdate struct {
// - Intensity: e.g. brightness, range 0..=1 // - Intensity: e.g. brightness, range 0..=1
// - Temperature: e.g. for thermostats // - Temperature: e.g. for thermostats
type DeviceState struct { 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 { type DeviceScene struct {
@ -83,11 +85,11 @@ func DeviceCapabilitiesToStrings(caps []DeviceCapability) []string {
var ( var (
DCPower DeviceCapability = "Power" DCPower DeviceCapability = "Power"
DCColorHS DeviceCapability = "ColorHS"
DCColorHS DeviceCapability = "HueSat"
DCColorHSK DeviceCapability = "ColorHSK" DCColorHSK DeviceCapability = "ColorHSK"
DCColorKelvin DeviceCapability = "ColorKelvin" DCColorKelvin DeviceCapability = "ColorKelvin"
DCColorXY DeviceCapability = "ColorXY"
DCColorRGB DeviceCapability = "ColorRGB"
DCColorXY DeviceCapability = "XY"
DCColorRGB DeviceCapability = "RGB"
DCButtons DeviceCapability = "Buttons" DCButtons DeviceCapability = "Buttons"
DCPresence DeviceCapability = "Presence" DCPresence DeviceCapability = "Presence"
DCIntensity DeviceCapability = "Intensity" DCIntensity DeviceCapability = "Intensity"
@ -128,7 +130,7 @@ func (d *Device) ApplyUpdate(update DeviceUpdate) {
func (d *Device) Validate() error { func (d *Device) Validate() error {
d.Name = strings.Trim(d.Name, " \t\n ") d.Name = strings.Trim(d.Name, " \t\n ")
if d.Name == "" { if d.Name == "" {
return ErrInvalidName
return lerrors.ErrInvalidName
} }
newCaps := make([]DeviceCapability, 0, len(d.Capabilities)) newCaps := make([]DeviceCapability, 0, len(d.Capabilities))
@ -186,7 +188,7 @@ func (d *Device) SetState(newState NewDeviceState) error {
} }
if newState.Color != nil { if newState.Color != nil {
parsed, err := ParseColorValue(*newState.Color)
parsed, err := color.Parse(*newState.Color)
if err != nil { if err != nil {
return err return err
} }
@ -196,7 +198,7 @@ func (d *Device) SetState(newState NewDeviceState) error {
d.State.Color = parsed d.State.Color = parsed
case parsed.K != nil && d.HasCapability(DCColorKelvin, DCColorHSK): case parsed.K != nil && d.HasCapability(DCColorKelvin, DCColorHSK):
if !d.HasCapability(DCColorKelvin) { 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 d.State.Color = parsed
@ -237,8 +239,8 @@ func (s *NewDeviceState) Interpolate(other NewDeviceState, fac float64) NewDevic
} }
if s.Color != nil && other.Color != nil { 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 { if err == nil && err2 == nil {
rc := sc.Interpolate(oc, fac) rc := sc.Interpolate(oc, fac)
rcStr := rc.String() rcStr := rc.String()

15
models/scene.go

@ -2,6 +2,7 @@ package models
import ( import (
"context" "context"
"git.aiterp.net/lucifer/new-server/internal/lerrors"
"math" "math"
"math/rand" "math/rand"
"sort" "sort"
@ -18,10 +19,10 @@ type Scene struct {
func (s *Scene) Validate() error { func (s *Scene) Validate() error {
if s.IntervalMS < 0 { if s.IntervalMS < 0 {
return ErrSceneInvalidInterval
return lerrors.ErrSceneInvalidInterval
} }
if len(s.Roles) == 0 { if len(s.Roles) == 0 {
return ErrSceneNoRoles
return lerrors.ErrSceneNoRoles
} }
for _, role := range s.Roles { for _, role := range s.Roles {
@ -118,31 +119,31 @@ func (d *SceneRunContext) IntervalFac() float64 {
func (r *SceneRole) Validate() error { func (r *SceneRole) Validate() error {
if len(r.States) == 0 { if len(r.States) == 0 {
return ErrSceneRoleNoStates
return lerrors.ErrSceneRoleNoStates
} }
switch r.TargetKind { switch r.TargetKind {
case RKTag, RKBridgeID, RKDeviceID, RKName, RKAll: case RKTag, RKBridgeID, RKDeviceID, RKName, RKAll:
default: default:
return ErrBadInput
return lerrors.ErrBadInput
} }
switch r.PowerMode { switch r.PowerMode {
case SPScene, SPDevice, SPBoth: case SPScene, SPDevice, SPBoth:
default: default:
return ErrSceneRoleUnknownPowerMode
return lerrors.ErrSceneRoleUnknownPowerMode
} }
switch r.Effect { switch r.Effect {
case SEStatic, SERandom, SEGradient, SEWalkingGradient, SETransition, SEMotion, SETemperature: case SEStatic, SERandom, SEGradient, SEWalkingGradient, SETransition, SEMotion, SETemperature:
default: default:
return ErrSceneRoleUnknownEffect
return lerrors.ErrSceneRoleUnknownEffect
} }
switch r.Order { switch r.Order {
case "", "-name", "name", "+name", "-id", "id", "+id": case "", "-name", "name", "+name", "-id", "id", "+id":
default: default:
return ErrSceneRoleUnsupportedOrdering
return lerrors.ErrSceneRoleUnsupportedOrdering
} }
return nil return nil

Loading…
Cancel
Save