Stian Aune
5 years ago
11 changed files with 717 additions and 1 deletions
-
16cmd/lucifer-server/main.go
-
58database/sqlite/bridge-repository.go
-
17database/sqlite/init.go
-
146database/sqlite/light-repository.go
-
4go.mod
-
8go.sum
-
29light/driver.go
-
169light/hue/driver.go
-
144light/service.go
-
24models/bridge.go
-
103models/light.go
@ -0,0 +1,58 @@ |
|||
package sqlite |
|||
|
|||
import ( |
|||
"context" |
|||
|
|||
"git.aiterp.net/lucifer/lucifer/models" |
|||
) |
|||
|
|||
type bridgeRepository struct{} |
|||
|
|||
// BridgeRepository is a sqlite datbase repository for the Bridge model.
|
|||
var BridgeRepository = &bridgeRepository{} |
|||
|
|||
func (b *bridgeRepository) FindByID(ctx context.Context, id int) (models.Bridge, error) { |
|||
bridge := models.Bridge{} |
|||
err := db.GetContext(ctx, &bridge, "SELECT * FROM bridge WHERE id=?", id) |
|||
|
|||
return bridge, err |
|||
} |
|||
|
|||
func (b *bridgeRepository) List(ctx context.Context) ([]models.Bridge, error) { |
|||
bridges := make([]models.Bridge, 0, 64) |
|||
err := db.SelectContext(ctx, &bridges, "SELECT * FROM bridge") |
|||
|
|||
return bridges, err |
|||
} |
|||
|
|||
func (b *bridgeRepository) ListByDriver(ctx context.Context, driver string) ([]models.Bridge, error) { |
|||
bridges := make([]models.Bridge, 0, 64) |
|||
err := db.SelectContext(ctx, &bridges, "SELECT * FROM bridge WHERE driver=?", driver) |
|||
|
|||
return bridges, err |
|||
} |
|||
|
|||
func (b *bridgeRepository) Insert(ctx context.Context, bridge models.Bridge) (models.Bridge, error) { |
|||
res, err := db.NamedExecContext(ctx, "INSERT INTO bridge (name, internal_id, driver, addr, key) VALUES(:name, :internal_id, :driver, :addr, :key)", bridge) |
|||
if err != nil { |
|||
return models.Bridge{}, err |
|||
} |
|||
|
|||
id, err := res.LastInsertId() |
|||
if err != nil { |
|||
return models.Bridge{}, err |
|||
} |
|||
|
|||
bridge.ID = int(id) |
|||
return bridge, nil |
|||
} |
|||
|
|||
func (b *bridgeRepository) Update(ctx context.Context, bridge models.Bridge) error { |
|||
_, err := db.NamedExecContext(ctx, "UPDATE bridge SET name=:name AND addr=:addr AND key=:key WHERE id=:id", bridge) |
|||
return err |
|||
} |
|||
|
|||
func (b *bridgeRepository) Remove(ctx context.Context, bridge models.Bridge) error { |
|||
_, err := db.NamedExecContext(ctx, "DELETE FROM bridge WHERE id=:id LIMIT 1", bridge) |
|||
return err |
|||
} |
@ -0,0 +1,146 @@ |
|||
package sqlite |
|||
|
|||
import ( |
|||
"context" |
|||
|
|||
"git.aiterp.net/lucifer/lucifer/models" |
|||
) |
|||
|
|||
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 |
|||
} |
|||
|
|||
return light, nil |
|||
} |
|||
|
|||
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 |
|||
} |
|||
|
|||
return light, nil |
|||
} |
|||
|
|||
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) |
|||
} |
|||
|
|||
return lights, nil |
|||
} |
|||
|
|||
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 |
|||
} |
|||
|
|||
lights = append(lights, light) |
|||
} |
|||
|
|||
return lights, nil |
|||
} |
|||
|
|||
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) |
|||
if err != nil { |
|||
return models.Light{}, err |
|||
} |
|||
|
|||
id, err := res.LastInsertId() |
|||
if err != nil { |
|||
return models.Light{}, err |
|||
} |
|||
|
|||
light.ID = int(id) |
|||
return light, nil |
|||
} |
|||
|
|||
func (r *lightRepository) Update(ctx context.Context, light models.Light) error { |
|||
panic("not implemented") |
|||
} |
|||
|
|||
func (r *lightRepository) Remove(ctx context.Context, light models.Light) error { |
|||
panic("not implemented") |
|||
} |
@ -1,11 +1,15 @@ |
|||
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 |
|||
github.com/mattn/go-sqlite3 v1.10.0 |
|||
golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b |
|||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 |
|||
google.golang.org/appengine v1.4.0 // indirect |
|||
gopkg.in/yaml.v2 v2.2.2 |
|||
) |
@ -0,0 +1,29 @@ |
|||
package light |
|||
|
|||
import ( |
|||
"context" |
|||
|
|||
"git.aiterp.net/lucifer/lucifer/models" |
|||
) |
|||
|
|||
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(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) |
|||
|
|||
// DiscoverBridges lists all available bridges.
|
|||
DiscoverBridges(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) |
|||
} |
|||
|
|||
// RegisterDriver registers a driver. This must happen in init() functions.
|
|||
func RegisterDriver(name string, driver Driver) { |
|||
drivers[name] = driver |
|||
} |
@ -0,0 +1,169 @@ |
|||
package hue |
|||
|
|||
import ( |
|||
"context" |
|||
"errors" |
|||
"log" |
|||
"sync" |
|||
"time" |
|||
|
|||
"git.aiterp.net/lucifer/lucifer/light" |
|||
"git.aiterp.net/lucifer/lucifer/models" |
|||
gohue "github.com/collinux/gohue" |
|||
"golang.org/x/sync/errgroup" |
|||
) |
|||
|
|||
// A driver is a driver for Phillips Hue lights.
|
|||
type driver struct { |
|||
mutex sync.Mutex |
|||
bridges map[int]*gohue.Bridge |
|||
} |
|||
|
|||
func (d *driver) Apply(ctx context.Context, bridge models.Bridge, lights ...models.Light) error { |
|||
hueBridge, err := d.getBridge(bridge) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
hueLights, err := hueBridge.GetAllLights() |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
eg, egCtx := errgroup.WithContext(ctx) |
|||
|
|||
for _, hueLight := range hueLights { |
|||
if !hueLight.State.Reachable { |
|||
continue |
|||
} |
|||
|
|||
for _, light := range lights { |
|||
if hueLight.UniqueID != light.InternalID { |
|||
continue |
|||
} |
|||
|
|||
// Prevent race condition since `hueLight` changes per iteration.
|
|||
hl := hueLight |
|||
|
|||
eg.Go(func() error { |
|||
select { |
|||
case <-egCtx.Done(): |
|||
return egCtx.Err() |
|||
default: |
|||
} |
|||
|
|||
return hl.SetState(gohue.LightState{ |
|||
On: light.On, |
|||
Hue: light.Color.Hue, |
|||
Sat: light.Color.Sat, |
|||
Bri: light.Color.Bri, |
|||
}) |
|||
}) |
|||
|
|||
break |
|||
} |
|||
} |
|||
|
|||
return eg.Wait() |
|||
} |
|||
|
|||
func (d *driver) DiscoverLights(ctx context.Context, bridge models.Bridge) ([]models.Light, error) { |
|||
hueBridge, err := d.getBridge(bridge) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
hueLights, err := hueBridge.GetAllLights() |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
lights := make([]models.Light, 0, len(hueLights)) |
|||
for _, hueLight := range hueLights { |
|||
lights = append(lights, 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, |
|||
}, |
|||
}) |
|||
} |
|||
|
|||
return lights, nil |
|||
} |
|||
|
|||
func (d *driver) DiscoverBridges(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) |
|||
} |
|||
|
|||
// Make 30 attempts (30 seconds)
|
|||
attempts := 30 |
|||
for attempts > 0 { |
|||
key, err := hueBridge.CreateUser("Lucifer (git.aiterp.net/lucifer/lucifer)") |
|||
if len(key) > 0 && err == nil { |
|||
bridge.Key = []byte(key) |
|||
bridge.InternalID = hueBridge.Info.Device.SerialNumber |
|||
|
|||
return bridge, nil |
|||
} |
|||
|
|||
select { |
|||
case <-time.After(time.Second): |
|||
attempts-- |
|||
case <-ctx.Done(): |
|||
return models.Bridge{}, ctx.Err() |
|||
} |
|||
} |
|||
|
|||
return models.Bridge{}, errors.New("Failed to create bridge") |
|||
} |
|||
|
|||
func (d *driver) getBridge(bridge models.Bridge) (*gohue.Bridge, error) { |
|||
d.mutex.Lock() |
|||
defer d.mutex.Unlock() |
|||
|
|||
if hueBridge, ok := d.bridges[bridge.ID]; ok { |
|||
return hueBridge, nil |
|||
} |
|||
|
|||
hueBridge, err := gohue.NewBridge(bridge.Addr) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
if err := hueBridge.GetInfo(); err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
if hueBridge.Info.Device.SerialNumber != bridge.InternalID { |
|||
return nil, errors.New("Serial number does not match hardware") |
|||
} |
|||
|
|||
err = hueBridge.Login(string(bridge.Key)) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
d.bridges[bridge.ID] = hueBridge |
|||
|
|||
return hueBridge, nil |
|||
} |
|||
|
|||
func init() { |
|||
driver := &driver{ |
|||
bridges: make(map[int]*gohue.Bridge, 16), |
|||
} |
|||
|
|||
light.RegisterDriver("hue", driver) |
|||
} |
@ -0,0 +1,144 @@ |
|||
package light |
|||
|
|||
import ( |
|||
"context" |
|||
"errors" |
|||
"log" |
|||
"time" |
|||
|
|||
"git.aiterp.net/lucifer/lucifer/models" |
|||
) |
|||
|
|||
// ErrUnknownDriver is returned by any function asking for a driver name if the driver specified doesn't exist.
|
|||
var ErrUnknownDriver = errors.New("Unknown driver specified") |
|||
|
|||
// A Service wraps the repos for lights and bridges and takes care of the business logic.
|
|||
type Service struct { |
|||
bridges models.BridgeRepository |
|||
lights models.LightRepository |
|||
} |
|||
|
|||
// DirectConnect connects to a bridge directly, without going through the discovery process to find them..
|
|||
func (s *Service) DirectConnect(ctx context.Context, driver string, addr string, name string) (models.Bridge, error) { |
|||
d, ok := drivers[driver] |
|||
if !ok { |
|||
return models.Bridge{}, ErrUnknownDriver |
|||
} |
|||
|
|||
bridge := models.Bridge{ |
|||
ID: -1, |
|||
Name: name, |
|||
Addr: addr, |
|||
Driver: driver, |
|||
} |
|||
|
|||
bridge, err := d.Connect(ctx, bridge) |
|||
if err != nil { |
|||
return models.Bridge{}, err |
|||
} |
|||
|
|||
bridge, err = s.bridges.Insert(ctx, bridge) |
|||
if err != nil { |
|||
return models.Bridge{}, err |
|||
} |
|||
|
|||
return bridge, nil |
|||
} |
|||
|
|||
// SyncLights syncs all lights in a bridge with the state in the database.
|
|||
func (s *Service) SyncLights(ctx context.Context, bridge models.Bridge) error { |
|||
d, ok := drivers[bridge.Driver] |
|||
if !ok { |
|||
return ErrUnknownDriver |
|||
} |
|||
|
|||
bridgeLights, err := d.DiscoverLights(ctx, bridge) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
dbLights, err := s.lights.ListByBridge(ctx, bridge) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
// Sync with matching db light if it exists.
|
|||
changedLights := make([]models.Light, 0, len(dbLights)) |
|||
|
|||
LightLoop: |
|||
for _, bridgeLight := range bridgeLights { |
|||
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 |
|||
} |
|||
} |
|||
|
|||
// 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 len(changedLights) > 0 { |
|||
err := d.Apply(ctx, bridge, changedLights...) |
|||
if err != nil { |
|||
log.Printf("Failed to apply one or more of the changes on bridge %d (%s): %s", bridge.ID, bridge.Name, err) |
|||
} |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// 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) |
|||
|
|||
for { |
|||
select { |
|||
case <-interval.C: |
|||
{ |
|||
bridges, err := s.Bridges(context.Background()) |
|||
if err != nil { |
|||
log.Println("Could not get bridges:", err) |
|||
} |
|||
|
|||
for _, bridge := range bridges { |
|||
err = s.SyncLights(ctx, bridge) |
|||
if err != nil { |
|||
log.Println("Sync failed:", err) |
|||
} |
|||
} |
|||
} |
|||
case <-ctx.Done(): |
|||
{ |
|||
log.Println("Sync loop stopped.") |
|||
return |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Bridge gets a bridge by ID.
|
|||
func (s *Service) Bridge(ctx context.Context, id int) (models.Bridge, error) { |
|||
return s.bridges.FindByID(ctx, id) |
|||
} |
|||
|
|||
// Bridges gets all known bridges.
|
|||
func (s *Service) Bridges(ctx context.Context) ([]models.Bridge, error) { |
|||
return s.bridges.List(ctx) |
|||
} |
|||
|
|||
// NewService creates a new light.Service.
|
|||
func NewService(bridges models.BridgeRepository, lights models.LightRepository) *Service { |
|||
return &Service{ |
|||
bridges: bridges, |
|||
lights: lights, |
|||
} |
|||
} |
@ -0,0 +1,24 @@ |
|||
package models |
|||
|
|||
import "context" |
|||
|
|||
// A Bridge is a device or service that holds lights.
|
|||
type Bridge struct { |
|||
ID int `json:"id" db:"id"` |
|||
Name string `json:"name" db:"name"` |
|||
Driver string `json:"driver" db:"driver"` |
|||
Addr string `json:"addr" db:"addr"` |
|||
InternalID string `json:"-" db:"internal_id"` |
|||
Key []byte `json:"-" db:"key"` |
|||
} |
|||
|
|||
// BridgeRepository is an interface for all database operations
|
|||
// the Bridge model makes.
|
|||
type BridgeRepository interface { |
|||
FindByID(ctx context.Context, id int) (Bridge, error) |
|||
List(ctx context.Context) ([]Bridge, error) |
|||
ListByDriver(ctx context.Context, driver string) ([]Bridge, error) |
|||
Insert(ctx context.Context, bridge Bridge) (Bridge, error) |
|||
Update(ctx context.Context, bridge Bridge) error |
|||
Remove(ctx context.Context, bridge Bridge) error |
|||
} |
@ -0,0 +1,103 @@ |
|||
package models |
|||
|
|||
import ( |
|||
"context" |
|||
"errors" |
|||
"fmt" |
|||
"strconv" |
|||
"strings" |
|||
|
|||
goColor "github.com/gerow/go-color" |
|||
) |
|||
|
|||
// 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 |
|||
} |
|||
|
|||
// 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) |
|||
} |
|||
|
|||
// String prints the color as HSB.
|
|||
func (color *LightColor) String() string { |
|||
return fmt.Sprintf("%d,%d,%d", color.Hue, color.Sat, color.Bri) |
|||
} |
|||
|
|||
// 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 |
|||
} |
|||
|
|||
// 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") |
|||
} |
|||
|
|||
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 |
|||
} |
|||
|
|||
color.Hue = uint16(hue) |
|||
color.Sat = uint8(sat) |
|||
color.Bri = uint8(bri) |
|||
|
|||
// 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 |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// LightRepository is an interface for all database operations
|
|||
// the Light model makes.
|
|||
type LightRepository interface { |
|||
FindByID(ctx context.Context, id int) (Light, error) |
|||
FindByInternalID(ctx context.Context, internalID string) (Light, error) |
|||
List(ctx context.Context) ([]Light, error) |
|||
ListByBridge(ctx context.Context, bridge Bridge) ([]Light, error) |
|||
Insert(ctx context.Context, light Light) (Light, error) |
|||
Update(ctx context.Context, light Light) error |
|||
Remove(ctx context.Context, light Light) error |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue