refactor color system to be separate from models, also move errors out of models.
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
+2
-1
@@ -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 {
|
||||||
|
|||||||
+4
-3
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-14
@@ -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,
|
lerrors.ErrNotFound: 404,
|
||||||
models.ErrInvalidName: 400,
|
lerrors.ErrInvalidName: 400,
|
||||||
models.ErrBadInput: 400,
|
lerrors.ErrBadInput: 400,
|
||||||
models.ErrBadColor: 400,
|
lerrors.ErrBadColor: 400,
|
||||||
models.ErrInternal: 500,
|
lerrors.ErrInternal: 500,
|
||||||
models.ErrUnknownColorFormat: 400,
|
lerrors.ErrUnknownColorFormat: 400,
|
||||||
|
|
||||||
models.ErrSceneInvalidInterval: 400,
|
lerrors.ErrSceneInvalidInterval: 400,
|
||||||
models.ErrSceneNoRoles: 400,
|
lerrors.ErrSceneNoRoles: 400,
|
||||||
models.ErrSceneRoleNoStates: 400,
|
lerrors.ErrSceneRoleNoStates: 400,
|
||||||
models.ErrSceneRoleUnsupportedOrdering: 422,
|
lerrors.ErrSceneRoleUnsupportedOrdering: 422,
|
||||||
models.ErrSceneRoleUnknownEffect: 422,
|
lerrors.ErrSceneRoleUnknownEffect: 422,
|
||||||
models.ErrSceneRoleUnknownPowerMode: 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+2
-8
@@ -1,21 +1,15 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"git.aiterp.net/lucifer/new-server/internal/color"
|
||||||
"git.aiterp.net/lucifer/new-server/models"
|
|
||||||
"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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
}
|
}
|
||||||
@@ -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),
|
||||||
}
|
}
|
||||||
@@ -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 {
|
type Gamut struct {
|
||||||
Red ColorXY `json:"red"`
|
Red XY `json:"red"`
|
||||||
Green ColorXY `json:"green"`
|
Green XY `json:"green"`
|
||||||
Blue ColorXY `json:"blue"`
|
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 {
|
func (xy XY) Round() XY {
|
||||||
return ColorXY{
|
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,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
|
||||||
|
|||||||
@@ -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,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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"`
|
Gamut color.Gamut `json:"gamut"`
|
||||||
GamutType string `json:"gamut_type"`
|
GamutType string `json:"gamut_type"`
|
||||||
XY models.ColorXY `json:"xy"`
|
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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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{
|
Color: color.Color{
|
||||||
HS: &models.ColorHS{
|
HS: &color.HueSat{
|
||||||
Hue: p.Hue,
|
Hue: p.Hue,
|
||||||
Sat: p.Sat,
|
Sat: p.Sat,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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,
|
Hue: c.HS.Hue,
|
||||||
Sat: color.HS.Sat,
|
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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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{}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package models
|
package lerrors
|
||||||
|
|
||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"`
|
ID int `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Value ColorValue `json:"value"`
|
Value color.Color `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ColorPresetRepository interface {
|
type ColorPresetRepository interface {
|
||||||
|
|||||||
@@ -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')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+14
-12
@@ -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"`
|
Power bool `json:"power"`
|
||||||
Color ColorValue `json:"color,omitempty"`
|
Color color.Color `json:"color,omitempty"`
|
||||||
Intensity float64 `json:"intensity,omitempty"`
|
Intensity float64 `json:"intensity,omitempty"`
|
||||||
Temperature int `json:"temperature"`
|
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"
|
DCColorXY DeviceCapability = "XY"
|
||||||
DCColorRGB DeviceCapability = "ColorRGB"
|
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)
|
sc, err := color.Parse(*s.Color)
|
||||||
oc, err2 := ParseColorValue(*other.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()
|
||||||
|
|||||||
+8
-7
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user