Squashed commit of the following:
continuous-integration/drone/push Build is passing

commit 62e7cb88f4
Author: Gisle Aune <dev@gisle.me>
Date:   Mon Feb 28 22:52:06 2022 +0100

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

commit df865574bf
Author: Gisle Aune <dev@gisle.me>
Date:   Mon Feb 21 19:27:42 2022 +0100

    rework color system and make it work with drivers.
This commit is contained in:
2022-02-28 22:52:25 +01:00
parent d5cf30ea4b
commit f1ed6da6d7
36 changed files with 681 additions and 376 deletions
+3 -2
View File
@@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"fmt"
"git.aiterp.net/lucifer/new-server/internal/lerrors"
"git.aiterp.net/lucifer/new-server/models"
"golang.org/x/sync/errgroup"
"io"
@@ -159,7 +160,7 @@ func (b *Bridge) ForgetDevice(ctx context.Context, device models.Device) error {
}
b.mu.Unlock()
if !found {
return models.ErrNotFound
return lerrors.ErrNotFound
}
// Delete light from bridge
@@ -237,7 +238,7 @@ func (b *Bridge) getToken(ctx context.Context) (string, error) {
return "", errLinkButtonNotPressed
}
if result[0].Success == nil {
return "", models.ErrUnexpectedResponse
return "", lerrors.ErrUnexpectedResponse
}
return result[0].Success.Username, nil
+2 -1
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"encoding/xml"
"fmt"
"git.aiterp.net/lucifer/new-server/internal/lerrors"
"git.aiterp.net/lucifer/new-server/models"
"log"
"net/http"
@@ -22,7 +23,7 @@ type Driver struct {
func (d *Driver) SearchBridge(ctx context.Context, address, _ string, dryRun bool) ([]models.Bridge, error) {
if address == "" {
if !dryRun {
return nil, models.ErrAddressOnlyDryRunnable
return nil, lerrors.ErrAddressOnlyDryRunnable
}
res, err := http.Get("https://discovery.meethue.com")
+4 -4
View File
@@ -40,7 +40,7 @@ func (s *hueLightState) Update(state models.DeviceState) {
if state.Power {
input.On = ptrBool(true)
if state.Color.IsKelvin() {
input.CT = ptrInt(1000000 / state.Color.Kelvin)
input.CT = ptrInt(1000000 / *state.Color.K)
if *input.CT < s.info.Capabilities.Control.CT.Min {
*input.CT = s.info.Capabilities.Control.CT.Min
}
@@ -51,13 +51,13 @@ func (s *hueLightState) Update(state models.DeviceState) {
if s.input.CT == nil || *s.input.CT != *input.CT {
s.stale = true
}
} else {
input.Hue = ptrInt(int(state.Color.Hue*(65536/360)) % 65536)
} else if color, ok := state.Color.ToHS(); ok {
input.Hue = ptrInt(int(color.HS.Hue*(65536/360)) % 65536)
if s.input.Hue == nil || *s.input.Hue != *input.Hue {
s.stale = true
}
input.Sat = ptrInt(int(state.Color.Saturation * 255))
input.Sat = ptrInt(int(color.HS.Sat * 255))
if *input.Sat > 254 {
*input.Sat = 254
}
+11 -10
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"git.aiterp.net/lucifer/new-server/internal/color"
"git.aiterp.net/lucifer/new-server/models"
"golang.org/x/sync/errgroup"
"log"
@@ -269,7 +270,7 @@ func (b *Bridge) MakeCongruent(ctx context.Context) (int, error) {
lightsOut := light.Power != nil && !device.State.Power
if !lightsOut {
if light.ColorTemperature != nil && device.State.Color.IsKelvin() {
mirek := 1000000 / device.State.Color.Kelvin
mirek := 1000000 / *device.State.Color.K
if mirek < light.ColorTemperature.MirekSchema.MirekMinimum {
mirek = light.ColorTemperature.MirekSchema.MirekMinimum
}
@@ -280,8 +281,8 @@ func (b *Bridge) MakeCongruent(ctx context.Context) (int, error) {
update.Mirek = &mirek
changed = true
}
} else if xy, ok := device.State.Color.ToXY(); ok && light.Color != nil {
xy = light.Color.Gamut.Conform(xy).Round()
} else if xyColor, ok := device.State.Color.ToXY(); ok && light.Color != nil {
xy := light.Color.Gamut.Conform(*xyColor.XY).Round()
if xy.DistanceTo(light.Color.XY) > 0.0009 || (light.ColorTemperature != nil && light.ColorTemperature.Mirek != nil) {
update.ColorXY = &xy
changed = true
@@ -419,9 +420,7 @@ func (b *Bridge) GenerateDevices() []models.Device {
}
if light.ColorTemperature != nil {
if light.ColorTemperature.Mirek != nil {
device.State.Color = models.ColorValue{
Kelvin: int(1000000 / *light.ColorTemperature.Mirek),
}
device.State.Color.SetK(1000000 / *light.ColorTemperature.Mirek)
}
device.Capabilities = append(device.Capabilities, models.DCColorKelvin)
device.DriverProperties["maxTemperature"] = 1000000 / light.ColorTemperature.MirekSchema.MirekMinimum
@@ -429,7 +428,7 @@ func (b *Bridge) GenerateDevices() []models.Device {
}
if light.Color != nil {
if device.State.Color.IsEmpty() {
device.State.Color = models.ColorValue{
device.State.Color = color.Color{
XY: &light.Color.XY,
}
}
@@ -530,9 +529,11 @@ func (b *Bridge) applyPatches(patches []ResourceData) {
resCopy.Color = &cp
resCopy.Color.XY = patch.Color.XY
cp2 := *resCopy.ColorTemperature
resCopy.ColorTemperature = &cp2
resCopy.ColorTemperature.Mirek = nil
if resCopy.ColorTemperature != nil {
cp2 := *resCopy.ColorTemperature
resCopy.ColorTemperature = &cp2
resCopy.ColorTemperature.Mirek = nil
}
}
if patch.ColorTemperature != nil && resCopy.ColorTemperature != nil {
cp := *resCopy.ColorTemperature
+2 -2
View File
@@ -8,7 +8,7 @@ import (
"encoding/json"
"errors"
"fmt"
"git.aiterp.net/lucifer/new-server/models"
"git.aiterp.net/lucifer/new-server/internal/lerrors"
"io"
"log"
"net"
@@ -52,7 +52,7 @@ func (c *Client) Register(ctx context.Context) (string, error) {
}
}
if result[0].Success == nil {
return "", models.ErrUnexpectedResponse
return "", lerrors.ErrUnexpectedResponse
}
c.token = result[0].Success.Username
+5 -5
View File
@@ -4,7 +4,7 @@ import (
"encoding/json"
"encoding/xml"
"fmt"
"git.aiterp.net/lucifer/new-server/models"
"git.aiterp.net/lucifer/new-server/internal/color"
"strings"
"time"
)
@@ -152,9 +152,9 @@ type LightDimming struct {
}
type LightColor struct {
Gamut models.ColorGamut `json:"gamut"`
GamutType string `json:"gamut_type"`
XY models.ColorXY `json:"xy"`
Gamut color.Gamut `json:"gamut"`
GamutType string `json:"gamut_type"`
XY color.XY `json:"xy"`
}
type LightCT struct {
@@ -182,7 +182,7 @@ type LightAlert struct {
type ResourceUpdate struct {
Name *string
Power *bool
ColorXY *models.ColorXY
ColorXY *color.XY
Brightness *float64
Mirek *int
TransitionDuration *time.Duration
+2 -1
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"encoding/xml"
"fmt"
"git.aiterp.net/lucifer/new-server/internal/lerrors"
"git.aiterp.net/lucifer/new-server/models"
"net/http"
"sync"
@@ -19,7 +20,7 @@ type Driver struct {
func (d *Driver) SearchBridge(ctx context.Context, address, token string, dryRun bool) ([]models.Bridge, error) {
if address == "" {
if !dryRun {
return nil, models.ErrAddressOnlyDryRunnable
return nil, lerrors.ErrAddressOnlyDryRunnable
}
res, err := http.Get("https://discovery.meethue.com")
+12 -8
View File
@@ -2,6 +2,8 @@ package lifx
import (
"context"
"git.aiterp.net/lucifer/new-server/internal/color"
"git.aiterp.net/lucifer/new-server/internal/lerrors"
"git.aiterp.net/lucifer/new-server/models"
"log"
"sync"
@@ -20,7 +22,7 @@ type Bridge struct {
func (b *Bridge) StartSearch(ctx context.Context) error {
c := b.getClient()
if c == nil {
return models.ErrBridgeRunningRequired
return lerrors.ErrBridgeRunningRequired
}
_, err := c.HorribleBroadcast(ctx, &GetService{})
@@ -129,9 +131,9 @@ func (b *Bridge) Run(ctx context.Context, debug bool) error {
for {
target, seq, payload, err := client.Recv(time.Millisecond * 200)
if err == models.ErrInvalidPacketSize || err == models.ErrPayloadTooShort || err == models.ErrUnrecognizedPacketType {
if err == lerrors.ErrInvalidPacketSize || err == lerrors.ErrPayloadTooShort || err == lerrors.ErrUnrecognizedPacketType {
log.Println("LIFX udp socket received something weird:", err)
} else if err != nil && err != models.ErrReadTimeout {
} else if err != nil && err != lerrors.ErrReadTimeout {
if ctx.Err() != nil {
return ctx.Err()
}
@@ -168,10 +170,12 @@ func (b *Bridge) Run(ctx context.Context, debug bool) error {
if state.deviceState == nil {
state.deviceState = &models.DeviceState{
Power: p.On,
Color: models.ColorValue{
Hue: p.Hue,
Saturation: p.Sat,
Kelvin: p.Kelvin,
Color: color.Color{
HS: &color.HueSat{
Hue: p.Hue,
Sat: p.Sat,
},
K: &p.Kelvin,
},
Intensity: p.Bri,
}
@@ -204,7 +208,7 @@ func (b *Bridge) Run(ctx context.Context, debug bool) error {
b.checkAndUpdateState(state)
}
if time.Since(state.discoveredTime) > time.Second*10 && time.Since(state.fwSpamTime) > time.Second * 30 {
if time.Since(state.discoveredTime) > time.Second*10 && time.Since(state.fwSpamTime) > time.Second*30 {
state.fwSpamTime = time.Now()
if state.firmware == nil {
+3 -3
View File
@@ -4,7 +4,7 @@ import (
"context"
"encoding/binary"
"errors"
"git.aiterp.net/lucifer/new-server/models"
"git.aiterp.net/lucifer/new-server/internal/lerrors"
"log"
"math/rand"
"net"
@@ -215,7 +215,7 @@ func (c *Client) Recv(timeout time.Duration) (target string, seq uint8, payload
}
if err != nil {
if netErr, ok := err.(*net.OpError); ok && netErr.Timeout() {
err = models.ErrReadTimeout
err = lerrors.ErrReadTimeout
return
}
@@ -224,7 +224,7 @@ func (c *Client) Recv(timeout time.Duration) (target string, seq uint8, payload
packet := Packet(c.buf[:n])
if n < 2 || packet.Size() != n && packet.Protocol() != 1024 {
err = models.ErrInvalidAddress
err = lerrors.ErrInvalidAddress
return
}
+3 -3
View File
@@ -3,7 +3,7 @@ package lifx
import (
"encoding/binary"
"fmt"
"git.aiterp.net/lucifer/new-server/models"
"git.aiterp.net/lucifer/new-server/internal/lerrors"
"log"
"net"
)
@@ -60,7 +60,7 @@ func (p *Packet) SetTarget(v string) error {
return err
}
if len(addr) != 6 {
return models.ErrInvalidAddress
return lerrors.ErrInvalidAddress
}
copy((*p)[8:], addr)
@@ -133,7 +133,7 @@ func (p *Packet) Payload() (res Payload, err error) {
res = &SetLightPower{}
err = res.Decode((*p)[36:])
default:
err = models.ErrUnrecognizedPacketType
err = lerrors.ErrUnrecognizedPacketType
}
if err != nil {
+7 -7
View File
@@ -3,7 +3,7 @@ package lifx
import (
"encoding/binary"
"fmt"
"git.aiterp.net/lucifer/new-server/models"
"git.aiterp.net/lucifer/new-server/internal/lerrors"
"math"
"time"
)
@@ -82,7 +82,7 @@ type StateHostFirmware struct {
func (p *StateHostFirmware) Decode(data []byte) error {
if len(data) < 20 {
return models.ErrPayloadTooShort
return lerrors.ErrPayloadTooShort
}
ts := int64(binary.LittleEndian.Uint64(data[0:8]))
@@ -123,7 +123,7 @@ type StateVersion struct {
func (p *StateVersion) Decode(data []byte) error {
if len(data) < 8 {
return models.ErrPayloadTooShort
return lerrors.ErrPayloadTooShort
}
p.Vendor = binary.LittleEndian.Uint32(data[0:4])
@@ -207,7 +207,7 @@ type SetColor struct {
func (p *SetColor) Decode(data []byte) error {
if len(data) < 13 {
return models.ErrPayloadTooShort
return lerrors.ErrPayloadTooShort
}
hue := binary.LittleEndian.Uint16(data[1:3])
@@ -263,7 +263,7 @@ type SetLightPower struct {
func (p *SetLightPower) Decode(data []byte) error {
if len(data) < 6 {
return models.ErrPayloadTooShort
return lerrors.ErrPayloadTooShort
}
level := binary.LittleEndian.Uint16(data[0:2])
@@ -314,7 +314,7 @@ func (p *StateService) String() string {
func (p *StateService) Decode(data []byte) error {
if len(data) < 5 {
return models.ErrPayloadTooShort
return lerrors.ErrPayloadTooShort
}
p.Service = int(data[0])
@@ -357,7 +357,7 @@ func (p *LightState) String() string {
func (p *LightState) Decode(data []byte) error {
if len(data) < 52 {
return models.ErrPayloadTooShort
return lerrors.ErrPayloadTooShort
}
hue := binary.LittleEndian.Uint16(data[0:2])
+18 -8
View File
@@ -1,6 +1,7 @@
package lifx
import (
"git.aiterp.net/lucifer/new-server/internal/color"
"git.aiterp.net/lucifer/new-server/models"
"math"
"time"
@@ -36,17 +37,21 @@ func (s *State) generateUpdate() []Payload {
return results
}
c := s.deviceState.Color
c, ok := s.deviceState.Color.ToHSK()
if !ok {
c, _ = color.Parse("hsk:0,0,4000")
}
l := s.lightState
di := s.deviceState.Intensity
k := c.Kelvin
k := *c.K
if k == 0 {
k = 4000
}
if !equalish(c.Hue, l.Hue) || !equalish(c.Saturation, l.Sat) || !equalish(di, l.Bri) || k != l.Kelvin {
if !equalish(c.HS.Hue, l.Hue) || !equalish(c.HS.Sat, l.Sat) || !equalish(di, l.Bri) || k != l.Kelvin {
results = append(results, &SetColor{
Hue: c.Hue,
Sat: c.Saturation,
Hue: c.HS.Hue,
Sat: c.HS.Sat,
Bri: di,
Kelvin: k,
TransitionTime: time.Millisecond * 150,
@@ -72,11 +77,16 @@ func (s *State) handleAck(seq uint8) {
prevLabel = s.lightState.Label
}
c, ok := s.deviceState.Color.ToHSK()
if !ok {
c, _ = color.Parse("hsk:0,0,4000")
}
s.lightState = &LightState{
Hue: s.deviceState.Color.Hue,
Sat: s.deviceState.Color.Saturation,
Hue: c.HS.Hue,
Sat: c.HS.Sat,
Bri: s.deviceState.Intensity,
Kelvin: s.deviceState.Color.Kelvin,
Kelvin: *c.K,
On: s.deviceState.Power,
Label: prevLabel,
}
+10 -9
View File
@@ -7,6 +7,7 @@ import (
"crypto/sha1"
"encoding/json"
"fmt"
"git.aiterp.net/lucifer/new-server/internal/lerrors"
"git.aiterp.net/lucifer/new-server/models"
"io"
"log"
@@ -163,9 +164,9 @@ func (b *bridge) command(ctx context.Context, command string, payload interface{
res, err := http.DefaultClient.Do(req)
if err != nil {
return models.ErrCannotForwardRequest
return lerrors.ErrCannotForwardRequest
} else if res.StatusCode != 200 {
return models.ErrIncorrectToken
return lerrors.ErrIncorrectToken
}
if target == nil {
@@ -174,7 +175,7 @@ func (b *bridge) command(ctx context.Context, command string, payload interface{
err = json.NewDecoder(res.Body).Decode(&target)
if err != nil {
return models.ErrUnexpectedResponse
return lerrors.ErrUnexpectedResponse
}
return nil
@@ -190,27 +191,27 @@ func (b *bridge) authenticate(ctx context.Context) error {
Password: b.password,
})
if err != nil {
return models.ErrMissingToken
return lerrors.ErrMissingToken
}
req, err := http.NewRequestWithContext(ctx, "POST", accountEndpoint + "login", bytes.NewReader(body))
req, err := http.NewRequestWithContext(ctx, "POST", accountEndpoint+"login", bytes.NewReader(body))
if err != nil {
return models.ErrMissingToken
return lerrors.ErrMissingToken
}
addDefaultHeaders(req)
res, err := http.DefaultClient.Do(req)
if err != nil {
return models.ErrCannotForwardRequest
return lerrors.ErrCannotForwardRequest
} else if res.StatusCode != 200 {
return models.ErrIncorrectToken
return lerrors.ErrIncorrectToken
}
var resBody authResBody
err = json.NewDecoder(res.Body).Decode(&resBody)
if err != nil {
return models.ErrBridgeSearchFailed
return lerrors.ErrBridgeSearchFailed
}
log.Printf("Mill: Authenticated as %s", resBody.NickName)
+26 -12
View File
@@ -5,12 +5,13 @@ import (
"context"
"encoding/json"
"fmt"
"git.aiterp.net/lucifer/new-server/internal/color"
"git.aiterp.net/lucifer/new-server/internal/lerrors"
"git.aiterp.net/lucifer/new-server/models"
"github.com/lucasb-eyer/go-colorful"
"io"
"io/ioutil"
"log"
"math"
"net"
"net/http"
"strconv"
@@ -31,11 +32,12 @@ type bridge struct {
func (b *bridge) Devices() []models.Device {
results := make([]models.Device, 0, len(b.panels))
for i, panel := range b.panels {
// Find normalized RGB and intensity
red := float64(panel.ColorRGBA[0]) / 255.0
green := float64(panel.ColorRGBA[1]) / 255.0
blue := float64(panel.ColorRGBA[2]) / 255.0
hue, sat, value := colorful.LinearRgb(red, green, blue).Hsv()
hue, sat, value := colorful.Color{R: red, G: green, B: blue}.Hsv()
rgb := colorful.Hsv(hue, sat, 1)
shapeType, shapeTypeOK := shapeTypeMap[panel.ShapeType]
if !shapeTypeOK {
@@ -70,10 +72,11 @@ func (b *bridge) Devices() []models.Device {
UserProperties: nil,
State: models.DeviceState{
Power: panel.ColorRGBA[3] == 0,
Color: models.ColorValue{
Hue: math.Mod(hue, 360),
Saturation: sat,
},
Color: color.Color{RGB: &color.RGB{
Red: rgb.R,
Green: rgb.G,
Blue: rgb.B,
}},
Intensity: value,
Temperature: 0,
},
@@ -139,9 +142,9 @@ func (b *bridge) Overview(ctx context.Context) (*Overview, error) {
switch res.StatusCode {
case 400, 403, 500, 503:
return nil, models.ErrUnexpectedResponse
return nil, lerrors.ErrUnexpectedResponse
case 401:
return nil, models.ErrIncorrectToken
return nil, lerrors.ErrIncorrectToken
}
overview := Overview{}
@@ -172,9 +175,20 @@ func (b *bridge) Update(devices []models.Device) {
for _, panel := range b.panels {
if panel.ID == uint16(id) {
if device.State.Power {
color := colorful.Hsv(device.State.Color.Hue, device.State.Color.Saturation, device.State.Intensity)
rgbColor, ok := device.State.Color.ToRGB()
if !ok {
newColor := [4]byte{255, 255, 255, 255}
if newColor != panel.ColorRGBA {
panel.update(newColor, time.Now().Add(time.Millisecond*220))
}
red, green, blue := color.RGB255()
continue
}
rgb := rgbColor.RGB.AtIntensity(device.State.Intensity)
red := byte(rgb.Red * 255.9)
green := byte(rgb.Green * 255.9)
blue := byte(rgb.Blue * 255.9)
newColor := [4]byte{red, green, blue, 255}
if newColor != panel.ColorRGBA {
panel.update(newColor, time.Now().Add(time.Millisecond*220))
@@ -321,7 +335,7 @@ func (b *bridge) updateEffect(ctx context.Context) error {
defer res.Body.Close()
if res.StatusCode != 204 {
return models.ErrUnexpectedResponse
return lerrors.ErrUnexpectedResponse
}
b.mu.Lock()
+3 -2
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"git.aiterp.net/lucifer/new-server/internal/lerrors"
"git.aiterp.net/lucifer/new-server/models"
"net/http"
"sync"
@@ -30,7 +31,7 @@ func (d *Driver) SearchBridge(ctx context.Context, address, _ string, dryRun boo
return nil, err
}
if deviceInfo.ModelNumber == "" {
return nil, models.ErrUnexpectedResponse
return nil, lerrors.ErrUnexpectedResponse
}
token := ""
@@ -47,7 +48,7 @@ func (d *Driver) SearchBridge(ctx context.Context, address, _ string, dryRun boo
defer res.Body.Close()
if res.StatusCode != 200 {
return nil, models.ErrBridgeSearchFailed
return nil, lerrors.ErrBridgeSearchFailed
}
tokenResponse := TokenResponse{}
+5 -3
View File
@@ -1,14 +1,16 @@
package drivers
import "git.aiterp.net/lucifer/new-server/models"
import (
"git.aiterp.net/lucifer/new-server/internal/lerrors"
"git.aiterp.net/lucifer/new-server/models"
)
type DriverMap map[models.DriverKind]models.Driver
func (m DriverMap) Provide(kind models.DriverKind) (models.Driver, error) {
if m[kind] == nil {
return nil, models.ErrUnknownDriver
return nil, lerrors.ErrUnknownDriver
}
return m[kind], nil
}