Browse Source

everything: Changing light colors works now.

login_bugfix
Gisle Aune 5 years ago
parent
commit
8c302ecbbe
  1. 21
      Dockerfile
  2. 56
      cmd/lucifer-server/main.go
  3. 78
      database/sqlite/group-repository.go
  4. 46
      database/sqlite/init.go
  5. 112
      database/sqlite/light-repository.go
  6. 2
      database/sqlite/user-repository.go
  7. 1
      go.mod
  8. 2
      go.sum
  9. 82
      internal/huecolor/convert.go
  10. 16
      internal/huecolor/convert_test.go
  11. 86
      internal/huecolor/gamut.go
  12. 17
      internal/huecolor/gamut_test.go
  13. 16
      light/driver.go
  14. 175
      light/hue/driver.go
  15. 60
      light/service.go
  16. 35
      models/group.go
  17. 102
      models/light.go

21
Dockerfile

@ -0,0 +1,21 @@
FROM golang:1.11 as build-server
WORKDIR /server
COPY . .
RUN go mod tidy
ENV CGO_ENABLED 1
RUN go build -ldflags "-w -s" ./cmd/lucifer-server/
FROM node:10.15.1-alpine as build-ui
WORKDIR /ui
COPY ./webui /ui
RUN npm install
RUN npm run build
FROM alpine:3.8
RUN apk add --no-cache sqlite
COPY /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2
RUN apk remove sqlite
COPY --from=build-server /server/lucifer-server /usr/local/bin/
COPY --from=build-ui /ui/build /usr/local/share/lucifer-ui
CMD ["/usr/local/bin/lucifer-server"]

56
cmd/lucifer-server/main.go

@ -2,10 +2,16 @@ package main
import (
"context"
"crypto/rand"
"database/sql"
"encoding/hex"
"fmt"
"log"
"net/http"
"time"
"git.aiterp.net/lucifer/lucifer/light"
"git.aiterp.net/lucifer/lucifer/models"
"github.com/gorilla/mux"
@ -29,6 +35,9 @@ func main() {
log.Fatalln("Failed to set up database:", err)
}
// Initialize
setupAdmin(sqlite.UserRepository, sqlite.GroupRepository)
// Services
lightService := light.NewService(sqlite.BridgeRepository, sqlite.LightRepository)
@ -42,7 +51,54 @@ func main() {
// Background Tasks
go lightService.SyncLoop(context.TODO())
//go lightService.DiscoverLoop(context.TODO())
// TODO: Listen in another goroutine and have SIGINT/SIGTERM handlers with graceful shutdown.
http.ListenAndServe(conf.Server.Address, router)
}
func setupAdmin(users models.UserRepository, groups models.GroupRepository) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
defer cancel()
admin, err := users.FindByName(ctx, "Admin")
if err != nil {
if err != sql.ErrNoRows {
log.Fatalln("Could not check for admin user:", err)
}
admin = models.User{Name: "Admin"}
admin.SetPassword("123456")
admin, err = users.Insert(ctx, admin)
if err != nil {
fmt.Println("Failed to insert admin username:", err)
}
}
buf := make([]byte, 16)
_, err = rand.Read(buf)
if err != nil {
log.Fatalln("Could not get random bytes:", err)
}
password := hex.EncodeToString(buf)
admin.SetPassword(password)
err = users.Update(ctx, admin)
if err != nil {
log.Println("Could not update admin password:", err)
} else {
log.Println("Administrator: Admin /", password)
}
groups.UpdatePermissions(ctx, models.GroupPermission{
UserID: admin.ID,
GroupID: 0,
Read: true,
Write: true,
Create: true,
Delete: true,
Manage: true,
})
}

78
database/sqlite/group-repository.go

@ -0,0 +1,78 @@
package sqlite
import (
"context"
"golang.org/x/sync/errgroup"
"git.aiterp.net/lucifer/lucifer/models"
)
type groupRepository struct{}
// GroupRepository is a sqlite datbase repository for the Group model.
var GroupRepository = &groupRepository{}
func (r *groupRepository) FindByID(ctx context.Context, id int) (models.Group, error) {
group := models.Group{}
err := db.GetContext(ctx, &group, "SELECT * FROM group WHERE id=?", id)
return group, err
}
func (r *groupRepository) FindByLight(ctx context.Context, light models.Light) (models.Group, error) {
group := models.Group{}
err := db.GetContext(ctx, &group, "SELECT g.id, g.name FROM `light` l JOIN `group` g ON g.id = l.group_id WHERE l.id = ?", light.ID)
return group, err
}
func (r *groupRepository) List(ctx context.Context) ([]models.Group, error) {
groups := make([]models.Group, 0, 16)
err := db.SelectContext(ctx, &groups, "SELECT * FROM group")
if err != nil {
return nil, err
}
eg, egCtx := errgroup.WithContext(ctx)
for i := range groups {
group := &groups[i]
eg.Go(func() error {
return db.SelectContext(egCtx, &group.Permissions, "SELECT * FROM group_permissions WHERE group_id=?", group.ID)
})
}
return groups, eg.Wait()
}
func (r *groupRepository) ListByUser(ctx context.Context, user models.User) ([]models.Group, error) {
panic("not implemented")
}
func (r *groupRepository) Insert(ctx context.Context, group models.Group) (models.Group, error) {
panic("not implemented")
}
func (r *groupRepository) Update(ctx context.Context, group models.Group) error {
_, err := db.NamedExecContext(ctx, "UPDATE group SET name=:name WHERE id=:id", group)
return err
}
func (r *groupRepository) UpdatePermissions(ctx context.Context, permission models.GroupPermission) error {
_, err := db.NamedExecContext(ctx, "REPLACE INTO group_permission(group_id, user_id, read, write, 'create', 'delete', manage) VALUES (:group_id, :user_id, :read, :write, :create, :delete, :manage)", permission)
return err
}
func (r *groupRepository) Remove(ctx context.Context, group models.Group) error {
_, err := db.NamedExecContext(ctx, "DELETE FROM group WHERE id=:id LIMIT 1", group)
return err
}
/*
SELECT g.id, g.name FROM group_permission AS p
JOIN 'group' AS g ON p.group_id=g.id
WHERE p.user_id=? AND p.read=true;
*/

46
database/sqlite/init.go

@ -1,6 +1,7 @@
package sqlite
import (
"log"
"strings"
"github.com/jmoiron/sqlx"
@ -31,9 +32,16 @@ func Initialize(filename string) (err error) {
}
tableDefs := strings.Split(tableDefStr, ";")
for _, tableDef := range tableDefs {
_, err := db.Exec(strings.Trim(tableDef, "\n  "))
for i, tableDef := range tableDefs {
query := strings.Trim(tableDef, "\n  ")
if len(query) < 2 {
continue
}
_, err := db.Exec(query)
if err != nil {
log.Printf("Statement %d failed", i)
db.Close()
db = nil
return err
@ -70,8 +78,36 @@ CREATE TABLE IF NOT EXISTS "light" (
id INTEGER PRIMARY KEY AUTOINCREMENT,
bridge_id INTEGER NOT NULL,
internal_id VARCHAR(255) NOT NULL,
group_id INTEGER NOT NULL,
name VARCHAR(255) NOT NULL,
'on' BOOLEAN NOT NULL,
color VARCHAR(255) NOT NULL
)
enabled BOOLEAN NOT NULL,
color VARCHAR(255) NOT NULL,
brightness INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS "group" (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(255) NOT NULL
);
CREATE TABLE IF NOT EXISTS "group_permission" (
group_id INT NOT NULL,
user_id INT NOT NULL,
read BOOLEAN NOT NULL,
write BOOLEAN NOT NULL,
'create' BOOLEAN NOT NULL,
'delete' BOOLEAN NOT NULL,
manage BOOLEAN NOT NULL,
PRIMARY KEY (group_id, user_id)
);
CREATE TABLE IF NOT EXISTS "group_light" (
group_id INT NOT NULL,
light_id INT NOT NULL,
PRIMARY KEY (group_id, light_id)
);
REPLACE INTO "group" (id, name) VALUES (0, "Lonely Lights");
`

112
database/sqlite/light-repository.go

@ -11,119 +11,43 @@ type lightRepository struct{}
// LightRepository is a sqlite datbase repository for the Light model.
var LightRepository = &lightRepository{}
type structScanner interface {
StructScan(v interface{}) error
}
func scanLight(row structScanner, light *models.Light) error {
scannedLight := struct {
ID int `db:"id"`
BridgeID int `db:"bridge_id"`
InternalID string `db:"internal_id"`
Name string `db:"name"`
On bool `db:"on"`
Color string `db:"color"`
}{}
if err := row.StructScan(&scannedLight); err != nil {
return err
}
light.ID = scannedLight.ID
light.BridgeID = scannedLight.BridgeID
light.InternalID = scannedLight.InternalID
light.Name = scannedLight.Name
light.On = scannedLight.On
return light.Color.Parse(scannedLight.Color)
}
func (r *lightRepository) FindByID(ctx context.Context, id int) (models.Light, error) {
row := db.QueryRowxContext(ctx, "SELECT * FROM light WHERE id=?", id)
if err := row.Err(); err != nil {
return models.Light{}, err
}
light := models.Light{}
if err := scanLight(row, &light); err != nil {
return models.Light{}, err
}
err := db.GetContext(ctx, &light, "SELECT * FROM light WHERE id=?", id)
return light, nil
return light, err
}
func (r *lightRepository) FindByInternalID(ctx context.Context, internalID string) (models.Light, error) {
row := db.QueryRowxContext(ctx, "SELECT * FROM light WHERE internal_id=?", internalID)
if err := row.Err(); err != nil {
return models.Light{}, err
}
light := models.Light{}
if err := scanLight(row, &light); err != nil {
return models.Light{}, err
}
err := db.GetContext(ctx, &light, "SELECT * FROM light WHERE internal_id=?", internalID)
return light, nil
return light, err
}
func (r *lightRepository) List(ctx context.Context) ([]models.Light, error) {
res, err := db.QueryxContext(ctx, "SELECT * FROM light")
if err != nil {
return nil, err
} else if err := res.Err(); err != nil {
return nil, err
}
lights := make([]models.Light, 0, 64)
for res.Next() {
light := models.Light{}
if err := scanLight(res, &light); err != nil {
return nil, err
}
lights = append(lights, light)
}
err := db.SelectContext(ctx, &lights, "SELECT * FROM light")
return lights, nil
return lights, err
}
func (r *lightRepository) ListByBridge(ctx context.Context, bridge models.Bridge) ([]models.Light, error) {
res, err := db.QueryxContext(ctx, "SELECT * FROM light WHERE bridge_id=?", bridge.ID)
if err != nil {
return nil, err
} else if err := res.Err(); err != nil {
return nil, err
}
lights := make([]models.Light, 0, 64)
for res.Next() {
light := models.Light{}
if err := scanLight(res, &light); err != nil {
return nil, err
}
err := db.SelectContext(ctx, &lights, "SELECT * FROM light WHERE bridge_id=?", bridge.ID)
lights = append(lights, light)
}
return lights, err
}
return lights, nil
func (r *lightRepository) ListByGroup(ctx context.Context, group models.Group) ([]models.Light, error) {
lights := make([]models.Light, 0, 64)
err := db.SelectContext(ctx, &lights, "SELECT * FROM light WHERE group_id=?", group.ID)
return lights, err
}
func (r *lightRepository) Insert(ctx context.Context, light models.Light) (models.Light, error) {
data := struct {
BridgeID int `db:"bridge_id"`
InternalID string `db:"internal_id"`
Name string `db:"name"`
On bool `db:"on"`
Color string `db:"color"`
}{
BridgeID: light.BridgeID,
InternalID: light.InternalID,
Name: light.Name,
On: light.On,
Color: light.Color.String(),
}
res, err := db.NamedExecContext(ctx, "INSERT INTO light (bridge_id, internal_id, name, `on`, color) VALUES(:bridge_id, :internal_id, :name, :on, :color)", data)
res, err := db.NamedExecContext(ctx, "INSERT INTO light (bridge_id, internal_id, group_id, name, enabled, color, brightness) VALUES(:bridge_id, :internal_id, :group_id, :name, :enabled, :color, :brightness)", light)
if err != nil {
return models.Light{}, err
}
@ -138,9 +62,11 @@ func (r *lightRepository) Insert(ctx context.Context, light models.Light) (model
}
func (r *lightRepository) Update(ctx context.Context, light models.Light) error {
panic("not implemented")
_, err := db.NamedExecContext(ctx, "UPDATE light SET group_id=:group_id, name=:name, enabled=:enabled, color=:color, brightness=:brightness WHERE id=:id", light)
return err
}
func (r *lightRepository) Remove(ctx context.Context, light models.Light) error {
panic("not implemented")
_, err := db.NamedExecContext(ctx, "DELETE FROM light WHERE id=:id", light)
return err
}

2
database/sqlite/user-repository.go

@ -76,7 +76,7 @@ func (repo *userRepository) Insert(ctx context.Context, user models.User) (model
}
func (repo *userRepository) Update(ctx context.Context, user models.User) error {
_, err := db.NamedExecContext(ctx, "UPDATE user SET name=:name AND hash=:hash WHERE id=:id", user)
_, err := db.NamedExecContext(ctx, "UPDATE user SET hash=:hash WHERE id=:id", user)
if err != nil {
return err
}

1
go.mod

@ -3,7 +3,6 @@ module git.aiterp.net/lucifer/lucifer
require (
github.com/collinux/GoHue v0.0.0-20181229002551-d259041d5eb8 // indirect
github.com/collinux/gohue v0.0.0-20181229002551-d259041d5eb8
github.com/gerow/go-color v0.0.0-20140219113758-125d37f527f1
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/mux v1.6.2
github.com/jmoiron/sqlx v1.2.0

2
go.sum

@ -2,8 +2,6 @@ github.com/collinux/GoHue v0.0.0-20181229002551-d259041d5eb8 h1:NOPuu1sMqBVC3iyl
github.com/collinux/GoHue v0.0.0-20181229002551-d259041d5eb8/go.mod h1:ex2AEcUvgoeoE/uknn0ZUExwGhBcH9jwA5heqv2xq6w=
github.com/collinux/gohue v0.0.0-20181229002551-d259041d5eb8 h1:Qhd31xZ6GUL0nEaXYP4nXOn8J6l9jqa6xEyp70qfjZE=
github.com/collinux/gohue v0.0.0-20181229002551-d259041d5eb8/go.mod h1:HFm7vkh/1EJQ9ymYsKUQtK7JlG3om1r61wMAHtl+bxw=
github.com/gerow/go-color v0.0.0-20140219113758-125d37f527f1 h1:DSA78HTfGC442ChonW9NdWuH5rsfJjTwsYwfIZhYjFo=
github.com/gerow/go-color v0.0.0-20140219113758-125d37f527f1/go.mod h1:uN90NshmoiEU0ECs3cPdEg3wshS8kG9Zez9RmYPuL5A=
github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=

82
internal/huecolor/convert.go

@ -0,0 +1,82 @@
package huecolor
import (
"math"
)
// ConvertXY converts the RGB value to XY and brightness and snaps it to the color gamut.
func ConvertXY(r, g, b float64) (x, y float64) {
x, y, z := convertXYZ(correctGamma(r, g, b))
return x / (x + y + z), y / (x + y + z)
}
// ConvertRGB converts back the values for ConvertXY to RGB. This does not gurantee that
// `ConvertRGB(ConvertXY(r, g, b))` returns the same `r`, `g` and `b` because of Gamut
// clamping.
func ConvertRGB(x, y, bri float64) (r, g, b float64) {
z := (1 - x - y)
y2 := bri
x2 := (y2 / y) * x
z2 := (y2 / y) * z
r = x2*1.4628067 - y2*0.1840623 - z2*0.2743606
g = -x2*0.5217933 + y2*1.4472381 + z2*0.0677227
b = x2*0.0349342 - y2*0.0968930 + z2*1.2884099
return reverseGamma(r, g, b)
}
// convertXYZ converts the RGB values to XYZ using the Wide RGB D65 conversion formula.
func convertXYZ(r, g, b float64) (x, y, z float64) {
return r*0.649926 + g*0.103455 + b*0.197109,
r*0.234327 + g*0.743075 + b*0.022598,
r*0.0000000 + g*0.053077 + b*1.035763
}
// correctGamma applies a gamma correction to the RGB values, which makes the color
// more vivid and more the like the color displayed on the screen of your device.
func correctGamma(r, g, b float64) (r2, g2, b2 float64) {
if r > 0.04045 {
r2 = math.Pow((r+0.055)/1.055, 2.4)
} else {
r2 = r / 12.92
}
if g > 0.04045 {
g2 = math.Pow((g+0.055)/1.055, 2.4)
} else {
g2 = g / 12.92
}
if b > 0.04045 {
b2 = math.Pow((b+0.055)/1.055, 2.4)
} else {
b2 = b / 12.92
}
return
}
// reverseGamma applies a reverse gamma correction to the RGB values to make them
func reverseGamma(r, g, b float64) (r2, g2, b2 float64) {
if r >= 0.0031308 {
r2 = 1.055*math.Pow(r, 1/2.4) - 0.055
} else {
r2 = r * 12.92
}
if g >= 0.0031308 {
g2 = 1.055*math.Pow(g, 1/2.4) - 0.055
} else {
g2 = g * 12.92
}
if b >= 0.0031308 {
b2 = 1.055*math.Pow(b, 1/2.4) - 0.055
} else {
b2 = b * 12.92
}
return
}

16
internal/huecolor/convert_test.go

@ -0,0 +1,16 @@
package huecolor_test
import (
"testing"
"git.aiterp.net/lucifer/lucifer/internal/huecolor"
)
func BenchmarkConvert(b *testing.B) {
for n := 0; n < b.N; n++ {
x, y := huecolor.ConvertXY(0.96, 0.04, 0.07)
if x < 0 && y < 0 {
b.Fail()
}
}
}

86
internal/huecolor/gamut.go

@ -0,0 +1,86 @@
package huecolor
// NOT IN USE. Gamuts just made things worse.
import "math"
type gamutPoint [2]float64
// A ColorGamut is a subset of a color space, and is used to constrain colors.
type ColorGamut struct {
R gamutPoint
G gamutPoint
B gamutPoint
}
var (
// HueGamut is the recommended gamut for hue bulbs
HueGamut = ColorGamut{gamutPoint{0.675, 0.322}, gamutPoint{0.4091, 0.518}, gamutPoint{0.167, 0.04}}
// FancyGamut is for LivingColors, Iris, and other fancy bulbs
FancyGamut = ColorGamut{gamutPoint{0.704, 0.296}, gamutPoint{0.2151, 0.7106}, gamutPoint{0.138, 0.08}}
// DefaultGamut is for non-Hue stuff.
DefaultGamut = ColorGamut{gamutPoint{0.9961, 0.0001}, gamutPoint{0.0001, 0.9961}, gamutPoint{0.0001, 0.0001}}
)
// Closest gets the closest point on the color gamut's edges. This function will always return a point
// on an edge, even if `(x, y)` is inside of it.
func (g *ColorGamut) Closest(x, y float64) (x2, y2 float64) {
p := gamutPoint{x, y}
pRG := closestToLine(g.R, g.G, p)
pGB := closestToLine(g.G, g.B, p)
pBR := closestToLine(g.B, g.R, p)
dRG := distance(p, pRG)
dGB := distance(p, pGB)
dBR := distance(p, pBR)
closest := pRG
closestDistance := dRG
if dGB < closestDistance {
closest = pGB
closestDistance = dGB
}
if dBR < closestDistance {
closest = pBR
closestDistance = dBR
}
return closest[0], closest[1]
}
// Contains retunrs true if the gamut contains the color.
func (g *ColorGamut) Contains(x, y float64) bool {
pt := gamutPoint{x, y}
b1 := sign(pt, g.R, g.G) < 0
b2 := sign(pt, g.G, g.B) < 0
b3 := sign(pt, g.B, g.R) < 0
return ((b1 == b2) && (b2 == b3))
}
func distance(p1, p2 gamutPoint) float64 {
dx := p1[0] - p2[0]
dy := p1[1] - p2[1]
return math.Sqrt(dx*dx + dy*dy)
}
func closestToLine(a, b, p gamutPoint) gamutPoint {
ap := gamutPoint{p[0] - a[0], p[1] - a[1]}
ab := gamutPoint{b[0] - a[0], b[1] - a[1]}
t := (ap[0]*ab[0] + ap[1]*ab[1]) / (ab[0]*ab[0] + ap[0]*ap[0])
if t < 0 {
t = 0
} else if t > 1 {
t = 1
}
return gamutPoint{a[0] + t*ab[0], a[1] + t*ab[1]}
}
func sign(p1, p2, p3 gamutPoint) float64 {
return (p1[0]-p3[0])*(p2[1]-p3[1]) - (p2[0]-p3[0])*(p1[1]-p3[1])
}

17
internal/huecolor/gamut_test.go

@ -0,0 +1,17 @@
package huecolor_test
import (
"testing"
"git.aiterp.net/lucifer/lucifer/internal/huecolor"
)
func TestGamut(t *testing.T) {
if !huecolor.DefaultGamut.Contains(0.1, 0.1) {
t.Error(1)
}
if huecolor.DefaultGamut.Contains(0, 0) {
t.Error(2)
}
}

16
light/driver.go

@ -10,17 +10,23 @@ var drivers = make(map[string]Driver)
// A Driver that communicates with an underlying lighting system.
type Driver interface {
// Apply applies all changed lights, which are lights that differ from what DiscoverLights returned.
// Apply applies all changed lights, which are lights that differ from what Lights returned.
Apply(ctx context.Context, bridge models.Bridge, lights ...models.Light) error
// DiscoverLights lists all available lights. The `ID` field will the -1.
DiscoverLights(ctx context.Context, bridge models.Bridge) ([]models.Light, error)
// Lights lists all available lights. The `ID` field will the -1.
Lights(ctx context.Context, bridge models.Bridge) ([]models.Light, error)
// DiscoverBridges lists all available bridges.
DiscoverBridges(ctx context.Context) ([]models.Bridge, error)
// Bridges lists all available bridges.
Bridges(ctx context.Context) ([]models.Bridge, error)
// Connect connects to a bridge, returning the bridge with the API Key.
Connect(ctx context.Context, bridge models.Bridge) (models.Bridge, error)
// DiscoverLights asks the bridge to start a search for new lights.
DiscoverLights(ctx context.Context, bridge models.Bridge) error
// ChangedLights returns a subset of the list describing which lights to update.
ChangedLights(ctx context.Context, bridge models.Bridge, lights ...models.Light) ([]models.Light, error)
}
// RegisterDriver registers a driver. This must happen in init() functions.

175
light/hue/driver.go

@ -2,21 +2,40 @@ package hue
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"sync"
"time"
"git.aiterp.net/lucifer/lucifer/internal/huecolor"
"git.aiterp.net/lucifer/lucifer/light"
"git.aiterp.net/lucifer/lucifer/models"
gohue "github.com/collinux/gohue"
"golang.org/x/sync/errgroup"
)
const (
// FlagUseXY applies a more aggressive mode change via xy to make TradFri bulbs work.
FlagUseXY = 1
)
type xyBri struct {
XY [2]float32
Bri uint8
}
func colorKey(light models.Light) string {
return fmt.Sprintf("%s.%s.%d", light.InternalID, light.Color, light.Brightness)
}
// A driver is a driver for Phillips Hue lights.
type driver struct {
mutex sync.Mutex
bridges map[int]*gohue.Bridge
colors map[string]xyBri
}
func (d *driver) Apply(ctx context.Context, bridge models.Bridge, lights ...models.Light) error {
@ -30,7 +49,7 @@ func (d *driver) Apply(ctx context.Context, bridge models.Bridge, lights ...mode
return err
}
eg, egCtx := errgroup.WithContext(ctx)
eg, _ := errgroup.WithContext(ctx)
for _, hueLight := range hueLights {
if !hueLight.State.Reachable {
@ -46,18 +65,38 @@ func (d *driver) Apply(ctx context.Context, bridge models.Bridge, lights ...mode
hl := hueLight
eg.Go(func() error {
select {
case <-egCtx.Done():
return egCtx.Err()
default:
if !light.On {
return hl.SetState(gohue.LightState{
On: false,
})
}
return hl.SetState(gohue.LightState{
x, y, bri, err := d.calcColor(light, hl)
if err != nil {
return err
}
log.Printf("Updating light (id: %d, rgb: %s, xy: [%f, %f], bri: %d)", light.ID, light.Color, x, y, bri)
err = hl.SetState(gohue.LightState{
On: light.On,
Hue: light.Color.Hue,
Sat: light.Color.Sat,
Bri: light.Color.Bri,
XY: &[2]float32{float32(x), float32(y)},
Bri: bri,
})
if err != nil {
return err
}
hl2, err := hueBridge.GetLightByIndex(hl.Index)
if err != nil {
return err
}
d.mutex.Lock()
d.colors[colorKey(light)] = xyBri{XY: hl2.State.XY, Bri: hl2.State.Bri}
d.mutex.Unlock()
return nil
})
break
@ -67,7 +106,16 @@ func (d *driver) Apply(ctx context.Context, bridge models.Bridge, lights ...mode
return eg.Wait()
}
func (d *driver) DiscoverLights(ctx context.Context, bridge models.Bridge) ([]models.Light, error) {
func (d *driver) DiscoverLights(ctx context.Context, bridge models.Bridge) error {
hueBridge, err := d.getBridge(bridge)
if err != nil {
return err
}
return hueBridge.FindNewLights()
}
func (d *driver) Lights(ctx context.Context, bridge models.Bridge) ([]models.Light, error) {
hueBridge, err := d.getBridge(bridge)
if err != nil {
return nil, err
@ -80,31 +128,32 @@ func (d *driver) DiscoverLights(ctx context.Context, bridge models.Bridge) ([]mo
lights := make([]models.Light, 0, len(hueLights))
for _, hueLight := range hueLights {
lights = append(lights, models.Light{
r, g, b := huecolor.ConvertRGB(float64(hueLight.State.XY[0]), float64(hueLight.State.XY[1]), float64(hueLight.State.Bri)/255)
light := models.Light{
ID: -1,
Name: hueLight.Name,
BridgeID: bridge.ID,
InternalID: hueLight.UniqueID,
On: hueLight.State.On,
Color: models.LightColor{
Hue: hueLight.State.Hue,
Sat: hueLight.State.Saturation,
Bri: hueLight.State.Bri,
},
})
}
light.SetColorRGBf(r, g, b)
light.Brightness = hueLight.State.Bri
lights = append(lights, light)
}
return lights, nil
}
func (d *driver) DiscoverBridges(ctx context.Context) ([]models.Bridge, error) {
func (d *driver) Bridges(ctx context.Context) ([]models.Bridge, error) {
panic("not implemented")
}
func (d *driver) Connect(ctx context.Context, bridge models.Bridge) (models.Bridge, error) {
hueBridge, err := gohue.NewBridge(bridge.Addr)
if err != nil {
log.Fatalln(err)
return models.Bridge{}, err
}
// Make 30 attempts (30 seconds)
@ -129,6 +178,84 @@ func (d *driver) Connect(ctx context.Context, bridge models.Bridge) (models.Brid
return models.Bridge{}, errors.New("Failed to create bridge")
}
func (d *driver) ChangedLights(ctx context.Context, bridge models.Bridge, lights ...models.Light) ([]models.Light, error) {
hueBridge, err := d.getBridge(bridge)
if err != nil {
return nil, err
}
hueLights, err := hueBridge.GetAllLights()
if err != nil {
return nil, err
}
subset := make([]models.Light, 0, len(lights))
for _, hueLight := range hueLights {
for _, light := range lights {
if hueLight.UniqueID != light.InternalID {
continue
}
d.mutex.Lock()
c, cOk := d.colors[colorKey(light)]
d.mutex.Unlock()
if !cOk || c.Bri != hueLight.State.Bri || diff(c.XY[0], hueLight.State.XY[0]) > 0.064 || diff(c.XY[1], hueLight.State.XY[1]) > 0.064 {
subset = append(subset, light)
}
break
}
}
return subset, nil
}
func (d *driver) calcColor(light models.Light, hueLight gohue.Light) (x, y float64, bri uint8, err error) {
r, g, b, err := light.ColorRGBf()
if err != nil {
return
}
x, y = huecolor.ConvertXY(r, g, b)
bri = light.Brightness
if bri < 1 {
bri = 1
} else if bri > 254 {
bri = 254
}
return
}
func (d *driver) getRawState(hueLight gohue.Light) (map[string]interface{}, error) {
data := struct {
State map[string]interface{} `json:"state"`
}{
State: make(map[string]interface{}, 16),
}
uri := fmt.Sprintf("/api/%s/lights/%d/", hueLight.Bridge.Username, hueLight.Index)
_, reader, err := hueLight.Bridge.Get(uri)
if err != nil {
return nil, err
}
err = json.NewDecoder(reader).Decode(&data)
return data.State, err
}
func (d *driver) setState(hueLight gohue.Light, key string, value interface{}) error {
m := make(map[string]interface{}, 1)
m[key] = value
uri := fmt.Sprintf("/api/%s/lights/%d/state", hueLight.Bridge.Username, hueLight.Index)
_, _, err := hueLight.Bridge.Put(uri, m)
return err
}
func (d *driver) getBridge(bridge models.Bridge) (*gohue.Bridge, error) {
d.mutex.Lock()
defer d.mutex.Unlock()
@ -160,9 +287,19 @@ func (d *driver) getBridge(bridge models.Bridge) (*gohue.Bridge, error) {
return hueBridge, nil
}
func diff(a, b float32) float32 {
diff := a - b
if diff < 0 {
return -diff
}
return diff
}
func init() {
driver := &driver{
bridges: make(map[int]*gohue.Bridge, 16),
colors: make(map[string]xyBri, 128),
}
light.RegisterDriver("hue", driver)

60
light/service.go

@ -52,7 +52,7 @@ func (s *Service) SyncLights(ctx context.Context, bridge models.Bridge) error {
return ErrUnknownDriver
}
bridgeLights, err := d.DiscoverLights(ctx, bridge)
bridgeLights, err := d.Lights(ctx, bridge)
if err != nil {
return err
}
@ -62,29 +62,31 @@ func (s *Service) SyncLights(ctx context.Context, bridge models.Bridge) error {
return err
}
// Sync with matching db light if it exists.
changedLights := make([]models.Light, 0, len(dbLights))
LightLoop:
// Add unknown lights
for _, bridgeLight := range bridgeLights {
found := false
for _, dbLight := range dbLights {
if dbLight.InternalID == bridgeLight.InternalID {
if !dbLight.Color.Equals(bridgeLight.Color) || dbLight.On != bridgeLight.On {
changedLights = append(changedLights, dbLight)
}
continue LightLoop
found = true
break
}
}
// Add unknown lights if it doesn't exist in the databse.
log.Println("Adding unknown light", bridgeLight.InternalID)
_, err := s.lights.Insert(ctx, bridgeLight)
if err != nil {
return err
if !found {
log.Println("Adding unknown light", bridgeLight.InternalID)
_, err := s.lights.Insert(ctx, bridgeLight)
if err != nil {
return err
}
}
}
changedLights, err := d.ChangedLights(ctx, bridge, dbLights...)
if err != nil {
return err
}
if len(changedLights) > 0 {
err := d.Apply(ctx, bridge, changedLights...)
if err != nil {
@ -98,7 +100,7 @@ LightLoop:
// SyncLoop runs synclight on all bridges twice every second until the context is
// done.
func (s *Service) SyncLoop(ctx context.Context) {
interval := time.NewTicker(time.Second / 2)
interval := time.NewTicker(time.Millisecond * 2500)
for {
select {
@ -125,6 +127,34 @@ func (s *Service) SyncLoop(ctx context.Context) {
}
}
// DiscoverLoop discovers stuff.
func (s *Service) DiscoverLoop(ctx context.Context) {
for {
bridges, err := s.Bridges(ctx)
if err != nil {
log.Println("Failed to get bridges:", err)
continue
}
for _, bridge := range bridges {
d, ok := drivers[bridge.Driver]
if !ok {
continue
}
log.Println("Searcing on bridge", bridge.Name)
err := d.DiscoverLights(ctx, bridge)
if err != nil {
log.Println("Failed to discover lights:", err)
continue
}
time.Sleep(time.Second * 60)
}
}
}
// Bridge gets a bridge by ID.
func (s *Service) Bridge(ctx context.Context, id int) (models.Bridge, error) {
return s.bridges.FindByID(ctx, id)

35
models/group.go

@ -0,0 +1,35 @@
package models
import "context"
// A Group is a collection of lights that relates to one another. The API should
// allow all lights within it.
type Group struct {
ID int `json:"id" db:"id"`
Name string `json:"name" db:"name"`
Permissions []GroupPermission `json:"permissions" db:"-"`
}
// A GroupPermission is a permission for a user in a group.
type GroupPermission struct {
GroupID int `json:"group_id" db:"group_id"`
UserID int `json:"user_id" db:"user_id"`
Read bool `json:"read" db:"read"`
Write bool `json:"write" db:"write"`
Create bool `json:"create" db:"create"`
Delete bool `json:"delete" db:"delete"`
Manage bool `json:"manage" db:"manage"`
}
// GroupRepository is an interface for all database operations
// the Group model makes.
type GroupRepository interface {
FindByID(ctx context.Context, id int) (Group, error)
FindByLight(ctx context.Context, light Light) (Group, error)
List(ctx context.Context) ([]Group, error)
ListByUser(ctx context.Context, user User) ([]Group, error)
UpdatePermissions(ctx context.Context, permission GroupPermission) error
Insert(ctx context.Context, group Group) (Group, error)
Update(ctx context.Context, group Group) error
Remove(ctx context.Context, group Group) error
}

102
models/light.go

@ -2,92 +2,57 @@ package models
import (
"context"
"encoding/hex"
"errors"
"fmt"
"strconv"
"strings"
goColor "github.com/gerow/go-color"
)
// ErrMalformedColor is returned by light.ColorRGBf when the color value is invalid.
var ErrMalformedColor = errors.New("Malformed color in light")
// A Light represents a bulb.
type Light struct {
ID int `json:"id"`
BridgeID int `json:"bridgeId"`
InternalID string `json:"-"`
Name string `json:"name"`
On bool `json:"on"`
Color LightColor `json:"color"`
}
// LightColor represent a HSB color.
type LightColor struct {
Hue uint16
Sat uint8
Bri uint8
ID int `json:"id" db:"id"`
BridgeID int `json:"bridgeId" db:"bridge_id"`
GroupID int `json:"groupId" db:"group_id"`
InternalID string `json:"internalId" db:"internal_id"`
Name string `json:"name" db:"name"`
On bool `json:"on" db:"enabled"`
Color string `json:"color" db:"color"`
Brightness uint8 `json:"brightness" db:"brightness"`
}
// SetRGB sets the light color's RGB.
func (color *LightColor) SetRGB(r, g, b uint8) {
rgb := goColor.RGB{
R: float64(r) / 255,
G: float64(g) / 255,
B: float64(b) / 255,
}
hsl := rgb.ToHSL()
color.Hue = uint16(hsl.H * 65535)
color.Sat = uint8(hsl.S * 255)
color.Bri = uint8(hsl.L * 255)
// SetColorRGB sets the color with an RGB value.
func (light *Light) SetColorRGB(r, g, b uint8) {
light.Color = hex.EncodeToString([]byte{r, g, b})
}
// String prints the color as HSB.
func (color *LightColor) String() string {
return fmt.Sprintf("%d,%d,%d", color.Hue, color.Sat, color.Bri)
// SetColorRGBf sets the color with an RGB value as floats between `0.0` and `1.0`.
func (light *Light) SetColorRGBf(r, g, b float64) {
light.SetColorRGB(uint8(r*255), uint8(g*255), uint8(b*255))
}
// Equals returns true if all the light's values are the same.
func (color *LightColor) Equals(other LightColor) bool {
return color.Hue == other.Hue && color.Sat == other.Sat && color.Bri == other.Bri
}
// ColorRGB gets the RGB values.
func (light *Light) ColorRGB() (r, g, b uint8, err error) {
// Parse parses three comma separated number.
func (color *LightColor) Parse(src string) error {
split := strings.SplitN(src, ",", 3)
if len(split) < 3 {
return errors.New("H,S,V format is incomplete")
if light.Color == "" {
return 0, 0, 0, nil
}
hue, err := strconv.ParseUint(split[0], 10, 16)
if err != nil {
return err
}
sat, err := strconv.ParseUint(split[1], 10, 16)
if err != nil {
return err
}
bri, err := strconv.ParseUint(split[2], 10, 16)
if err != nil {
return err
bytes, err := hex.DecodeString(light.Color)
if err != nil || len(bytes) != 3 {
return 0, 0, 0, ErrMalformedColor
}
color.Hue = uint16(hue)
color.Sat = uint8(sat)
color.Bri = uint8(bri)
return bytes[0], bytes[1], bytes[2], nil
}
// Hue doesn't like 0 and 255 for Sat and Bri.
if color.Sat < 1 {
color.Sat = 1
} else if color.Sat > 254 {
color.Sat = 254
}
if color.Bri < 1 {
color.Bri = 1
} else if color.Bri > 254 {
color.Bri = 254
// ColorRGBf returns the float values of the RGB color.
func (light *Light) ColorRGBf() (r, g, b float64, err error) {
r8, g8, b8, err := light.ColorRGB()
if err != nil {
return 0, 0, 0, err
}
return nil
return float64(r8) / 255, float64(g8) / 255, float64(b8) / 255, nil
}
// LightRepository is an interface for all database operations
@ -97,6 +62,7 @@ type LightRepository interface {
FindByInternalID(ctx context.Context, internalID string) (Light, error)
List(ctx context.Context) ([]Light, error)
ListByBridge(ctx context.Context, bridge Bridge) ([]Light, error)
ListByGroup(ctx context.Context, group Group) ([]Light, error)
Insert(ctx context.Context, light Light) (Light, error)
Update(ctx context.Context, light Light) error
Remove(ctx context.Context, light Light) error

Loading…
Cancel
Save