Browse Source

add lifx driver. It works with the bulb, but the code is a bit smelly.

pull/1/head
Gisle Aune 4 years ago
parent
commit
95ec918ed1
  1. 2
      app/config/driver.go
  2. 45
      cmd/bridgetest/main.go
  3. 267
      internal/drivers/lifx/bridge.go
  4. 178
      internal/drivers/lifx/client.go
  5. 177
      internal/drivers/lifx/driver.go
  6. 191
      internal/drivers/lifx/packet.go
  7. 417
      internal/drivers/lifx/payloads.go
  8. 73
      internal/drivers/lifx/products.go
  9. 87
      internal/drivers/lifx/state.go
  10. 1
      models/bridge.go
  11. 22
      models/colorvalue.go
  12. 11
      models/device.go
  13. 7
      models/errors.go

2
app/config/driver.go

@ -3,6 +3,7 @@ package config
import ( import (
"git.aiterp.net/lucifer/new-server/internal/drivers" "git.aiterp.net/lucifer/new-server/internal/drivers"
"git.aiterp.net/lucifer/new-server/internal/drivers/hue" "git.aiterp.net/lucifer/new-server/internal/drivers/hue"
"git.aiterp.net/lucifer/new-server/internal/drivers/lifx"
"git.aiterp.net/lucifer/new-server/internal/drivers/nanoleaf" "git.aiterp.net/lucifer/new-server/internal/drivers/nanoleaf"
"git.aiterp.net/lucifer/new-server/models" "git.aiterp.net/lucifer/new-server/models"
"sync" "sync"
@ -19,6 +20,7 @@ func DriverProvider() models.DriverProvider {
dp = drivers.DriverMap{ dp = drivers.DriverMap{
models.DTNanoLeaf: &nanoleaf.Driver{}, models.DTNanoLeaf: &nanoleaf.Driver{},
models.DTHue: &hue.Driver{}, models.DTHue: &hue.Driver{},
models.DTLIFX: &lifx.Driver{},
} }
} }

45
cmd/bridgetest/main.go

@ -47,6 +47,15 @@ func main() {
log.Println("New token:", bridge.Token) log.Println("New token:", bridge.Token)
} }
ch := config.EventChannel
go func() {
err := driver.Run(context.Background(), bridge, ch)
if err != nil {
log.Fatalln("Run bridge stopped:", err)
}
}()
time.Sleep(time.Second)
// List devices // List devices
var devices []models.Device var devices []models.Device
if *flagSearch { if *flagSearch {
@ -60,18 +69,14 @@ func main() {
log.Fatalln("Failed to list devices:", err) log.Fatalln("Failed to list devices:", err)
} }
} }
idMap := make(map[string]int)
nextId := len(devices) + 1
for i := range devices { for i := range devices {
devices[i].ID = i + 1 devices[i].ID = i + 1
idMap[devices[i].InternalID] = i + 1
} }
ch := config.EventChannel
go func() {
err := driver.Run(context.Background(), bridge, ch)
if err != nil {
log.Fatalln("Run bridge stopped:", err)
}
}()
go func() { go func() {
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
@ -85,6 +90,30 @@ func main() {
text, _ := reader.ReadString('\n') text, _ := reader.ReadString('\n')
text = strings.Trim(text, "\t  \r\n") text = strings.Trim(text, "\t  \r\n")
if text == "search" {
_, _ = driver.SearchDevices(context.Background(), bridge, time.Second)
}
if text == "list" || text == "search" || text == "json" {
devices, err = driver.ListDevices(context.Background(), bridge)
if err != nil {
log.Fatalln("Failed to list devices:", err)
}
for i, device := range devices {
if extId, ok := idMap[device.InternalID]; ok {
devices[i].ID = extId
} else {
idMap[device.InternalID] = nextId
devices[i].ID = nextId
nextId += 1
}
if text != "json" {
_, _ = fmt.Fprintf(os.Stderr, "Device: %d - %s %+v\n", device.ID, device.InternalID, device.Capabilities)
}
}
}
if text == "json" { if text == "json" {
j, _ := json.MarshalIndent(devices, "", " ") j, _ := json.MarshalIndent(devices, "", " ")
fmt.Println(string(j)) fmt.Println(string(j))

267
internal/drivers/lifx/bridge.go

@ -0,0 +1,267 @@
package lifx
import (
"context"
"git.aiterp.net/lucifer/new-server/models"
"log"
"sync"
"time"
)
type Bridge struct {
mu sync.Mutex
externalID int
ip string
states []*State
client *Client
}
func (b *Bridge) StartSearch() error {
c := b.getClient()
if c == nil {
return models.ErrBridgeRunningRequired
}
_, err := c.Send("", &GetService{})
return err
}
func (b *Bridge) Publish(devices []models.Device) {
b.mu.Lock()
for _, device := range devices {
state, _ := b.ensureState(device.InternalID)
state.deviceState = new(models.DeviceState)
*state.deviceState = device.State
b.checkAndUpdateState(state)
state.externalId = device.ID
}
b.mu.Unlock()
}
func (b *Bridge) Devices() []models.Device {
devices := make([]models.Device, 0, 8)
b.mu.Lock()
for _, state := range b.states {
if state.lightState == nil || state.firmware == nil || state.version == nil {
continue
}
deviceState := models.DeviceState{}
if state.deviceState != nil {
deviceState = *state.deviceState
}
device := models.Device{
ID: state.externalId,
BridgeID: b.externalID,
InternalID: state.target,
Icon: "lightbulb",
Name: state.lightState.Label,
Capabilities: []models.DeviceCapability{models.DCPower, models.DCIntensity},
DriverProperties: make(map[string]interface{}),
State: deviceState,
}
device.DriverProperties["lifxVendorId"] = state.version.Vendor
device.DriverProperties["lifxProductId"] = state.version.Product
device.DriverProperties["lifxFirmwareMajor"] = state.firmware.Major
device.DriverProperties["lifxFirmwareMinor"] = state.firmware.Minor
device.DriverProperties["lifxFirmwareBuild"] = state.firmware.BuildTime
product := findProduct(state.version.Vendor, state.version.Product)
if product != nil {
device.DriverProperties["productName"] = product.Name
if product.Features.Color {
device.Capabilities = append(device.Capabilities, models.DCColorHS)
}
if len(product.Features.TemperatureRange) >= 2 {
device.Capabilities = append(device.Capabilities, models.DCColorHSK, models.DCColorKelvin)
device.DriverProperties["minKelvin"] = product.Features.TemperatureRange[0]
device.DriverProperties["maxKelvin"] = product.Features.TemperatureRange[1]
}
for _, upgrade := range product.Upgrades {
if state.firmware.Major > upgrade.Major || (state.firmware.Major >= upgrade.Major && state.firmware.Minor >= upgrade.Minor) {
if upgrade.Features.TemperatureRange != nil {
device.DriverProperties["minKelvin"] = upgrade.Features.TemperatureRange[0]
device.DriverProperties["maxKelvin"] = upgrade.Features.TemperatureRange[1]
}
}
}
}
devices = append(devices, device)
}
b.mu.Unlock()
return devices
}
func (b *Bridge) Run(ctx context.Context, debug bool) error {
client, err := createClient(ctx, b.ip, debug)
if err != nil {
return err
}
b.mu.Lock()
b.client = client
b.mu.Unlock()
defer func() {
b.mu.Lock()
if b.client == client {
b.client = nil
}
b.mu.Unlock()
}()
lastSearchTime := time.Now()
lastServiceTime := time.Time{}
_, err = client.Send("", &GetService{})
if err != nil {
return err
}
for {
target, seq, payload, err := client.Recv(time.Millisecond * 200)
if err == models.ErrInvalidPacketSize || err == models.ErrPayloadTooShort || err == models.ErrUnrecognizedPacketType {
log.Println("LIFX udp socket received something weird:", err)
} else if err != nil && err != models.ErrReadTimeout {
if ctx.Err() != nil {
return ctx.Err()
}
return err
}
if payload != nil {
b.mu.Lock()
state, _ := b.ensureState(target)
b.mu.Unlock()
switch p := payload.(type) {
case *StateService:
if p.Service == 1 {
// Throw these messages to the wind. UDP errors will eventually be caught.
// Get the version only if it's missing. It should never change anyway.
b.mu.Lock()
if state.version == nil {
_, _ = client.Send(target, &GetVersion{})
}
b.mu.Unlock()
_, _ = client.Send(target, &GetHostFirmware{})
lastServiceTime = time.Now()
}
case *LightState:
b.mu.Lock()
state.lightState = p
state.lightStateTime = time.Now()
if state.deviceState == nil {
state.deviceState = &models.DeviceState{
Power: p.On,
Color: models.ColorValue{
Hue: p.Hue,
Saturation: p.Sat,
Kelvin: p.Kelvin,
},
Intensity: p.Bri,
}
}
b.checkAndUpdateState(state)
b.mu.Unlock()
case *StateHostFirmware:
b.mu.Lock()
state.firmware = p
b.mu.Unlock()
case *StateVersion:
b.mu.Lock()
state.version = p
b.mu.Unlock()
case *Acknowledgement:
b.mu.Lock()
state.handleAck(seq)
b.mu.Unlock()
}
}
b.mu.Lock()
for _, state := range b.states {
if time.Since(state.lightStateTime) > time.Second*10 && time.Since(state.requestTime) > time.Second*3 {
state.requestTime = time.Now()
_, _ = client.Send(state.target, &GetColor{})
} else if len(state.acksPending) > 0 && time.Since(state.updateTime) > time.Second {
state.requestTime = time.Now()
b.checkAndUpdateState(state)
}
if time.Since(state.discoveredTime) > time.Second*10 {
if state.firmware == nil {
_, _ = client.Send(state.target, &GetHostFirmware{})
}
if state.version == nil {
_, _ = client.Send(state.target, &GetVersion{})
}
}
}
b.mu.Unlock()
//
if time.Since(lastServiceTime) > time.Second*30 && time.Since(lastSearchTime) > time.Second*3 {
lastSearchTime = time.Now()
_, err = client.Send("", &GetService{})
if err != nil {
return err
}
}
}
}
func (b *Bridge) checkAndUpdateState(state *State) {
state.acksPending = state.acksPending[:0]
updatePayloads := state.generateUpdate()
for _, updatePayload := range updatePayloads {
seq, err := b.client.Send(state.target, updatePayload)
if err != nil {
log.Println("Error sending updates to", state.externalId, state.target, "err:", err)
continue
}
state.updateTime = time.Now()
state.acksPending = append(state.acksPending, seq)
}
}
func (b *Bridge) ensureState(target string) (*State, bool) {
for _, state := range b.states {
if state.target == target {
return state, false
}
}
state := &State{
target: target,
discoveredTime: time.Now(),
}
b.states = append(b.states, state)
return state, true
}
func (b *Bridge) getClient() *Client {
b.mu.Lock()
client := b.client
b.mu.Unlock()
return client
}

178
internal/drivers/lifx/client.go

@ -0,0 +1,178 @@
package lifx
import (
"context"
"errors"
"git.aiterp.net/lucifer/new-server/models"
"log"
"math/rand"
"net"
"sync"
"time"
)
type Client struct {
mu sync.Mutex
seq uint8
buf []byte
lastN int
conn *net.UDPConn
source uint32
debug bool
addrMap map[string]*net.UDPAddr
}
// Send sends the payload to the hardware addr specified. If it cannot find a mapped
// IP address from a past message, it will be broadcast and may be slow to use. If the
// address is left blank, the package is marked as tagged and broadcast.
//
// ErrInvalidAddress can be returned by this, and indicates a badly configured device.
// It should probably be logged.
func (c *Client) Send(addr string, payload Payload) (seq uint8, err error) {
c.mu.Lock()
seq = c.seq
c.seq += 1
c.mu.Unlock()
packet := createPacket()
packet.SetSource(c.source)
packet.SetSequence(seq)
sendAddr := &net.UDPAddr{IP: net.IPv4(255, 255, 255, 255), Port: 56700}
if addr == "" {
packet.SetTagged(true)
if c.debug {
log.Println("Broadcasting", payload, "seq", seq)
}
} else {
err = packet.SetTarget(addr)
if err != nil {
return
}
c.mu.Lock()
if udpAddr, ok := c.addrMap[addr]; ok {
sendAddr = udpAddr
}
c.mu.Unlock()
if c.debug {
log.Println("Sending", payload, "to", addr, "seq", seq)
}
}
packet.SetPayload(payload)
_, err = c.conn.WriteToUDP(packet, sendAddr)
if err != nil {
return
}
return
}
// LastPacket gets the last read packet. This data is valid until the next call to Recv. The Packet may
// not be valid, however!
func (c *Client) LastPacket() Packet {
if c.lastN < 36 {
return nil
}
return c.buf[:c.lastN]
}
// Recv reads a message from the UDP socket. The data returned is decoded and will always be valid.
//
// However, these should be handled specifically:
// - ErrPayloadTooShort, ErrInvalidPacketSize: Garbage was received, please ignore.
// - ErrUnrecognizedPacketType: Log these and see what's up
// - ErrReadTimeout: The connection gets a 50ms read deadline. It should be used to do other things than wait
func (c *Client) Recv(timeout time.Duration) (target string, seq uint8, payload Payload, err error) {
if c.buf == nil {
c.buf = make([]byte, 2048)
}
if timeout > 0 {
err = c.conn.SetReadDeadline(time.Now().Add(timeout))
} else {
err = c.conn.SetReadDeadline(time.Time{})
}
if err != nil {
return
}
n, addr, err := c.conn.ReadFromUDP(c.buf)
if n > 0 {
c.lastN = n
}
if err != nil {
if netErr, ok := err.(*net.OpError); ok && netErr.Timeout() {
err = models.ErrReadTimeout
return
}
return
}
packet := Packet(c.buf[:n])
if n < 2 || packet.Size() != n && packet.Protocol() != 1024 {
err = models.ErrInvalidAddress
return
}
seq = packet.Sequence()
target = packet.Target().String()
payload, err = packet.Payload()
if err != nil {
return
}
// Learn the IP address from state service or ack messages.
if service, ok := payload.(*StateService); ok && service.Service == 1 {
c.mu.Lock()
if c.addrMap == nil {
c.addrMap = make(map[string]*net.UDPAddr)
}
c.addrMap[packet.Target().String()] = addr
c.mu.Unlock()
}
if c.debug {
log.Println("Received", payload, "from", target, "seq", seq)
}
return
}
// createClient creates a client that will last as long as the context.
func createClient(ctx context.Context, bindAddr string, debug bool) (*Client, error) {
addr := net.ParseIP(bindAddr)
if addr == nil {
return nil, errors.New("invalid addr")
}
source := uint32(rand.Uint64())
if source < 2 {
source = 2
}
conn, err := net.ListenUDP("udp", &net.UDPAddr{
IP: addr,
Port: 0,
Zone: "",
})
if err != nil {
return nil, err
}
go func() {
<-ctx.Done()
_ = conn.Close()
}()
return &Client{conn: conn, source: source, debug: debug}, nil
}

177
internal/drivers/lifx/driver.go

@ -0,0 +1,177 @@
package lifx
import (
"context"
"fmt"
"git.aiterp.net/lucifer/new-server/models"
"net"
"sync"
"time"
)
type Driver struct {
mu sync.Mutex
bridges []*Bridge
}
func (d *Driver) SearchBridge(ctx context.Context, address string, _ bool) ([]models.Bridge, error) {
if address == "" {
ifaces, err := net.Interfaces()
if err != nil {
return nil, err
}
bridges := make([]models.Bridge, 0, len(ifaces))
for _, iface := range ifaces {
if iface.Name == "lo" {
continue
}
addrs, err := iface.Addrs()
if err != nil || len(addrs) == 0 {
continue
}
for _, addr := range addrs {
bridges = append(bridges, models.Bridge{
ID: -1,
Name: fmt.Sprintf("%s (%s)", iface.Name, addr),
Driver: models.DTLIFX,
Address: addr.String(),
})
}
}
return bridges, nil
}
ctx2, cancel := context.WithCancel(ctx)
defer cancel()
_, err := createClient(ctx2, address, false)
if err != nil {
return nil, err
}
return []models.Bridge{{
ID: 0,
Name: "Your network card",
Driver: models.DTLIFX,
Address: address,
Token: "",
}}, nil
}
func (d *Driver) SearchDevices(ctx context.Context, bridge models.Bridge, timeout time.Duration) ([]models.Device, error) {
b, err := d.ensureBridge(ctx, bridge)
if err != nil {
return nil, err
}
before, err := d.ListDevices(ctx, bridge)
if err != nil {
return nil, err
}
err = b.StartSearch()
if err != nil {
return nil, err
}
if timeout < time.Second/10 {
timeout = time.Second / 10
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(timeout):
}
after, err := d.ListDevices(ctx, bridge)
if err != nil {
return nil, err
}
intersection := make([]models.Device, 0, 4)
for _, device := range after {
found := false
for _, device2 := range before {
if device2.InternalID == device.InternalID {
found = true
break
}
}
if !found {
intersection = append(intersection, device)
}
}
return intersection, nil
}
func (d *Driver) ListDevices(ctx context.Context, bridge models.Bridge) ([]models.Device, error) {
b, err := d.ensureBridge(ctx, bridge)
if err != nil {
return nil, err
}
return b.Devices(), nil
}
func (d *Driver) Publish(ctx context.Context, bridge models.Bridge, devices []models.Device) error {
b, err := d.ensureBridge(ctx, bridge)
if err != nil {
return err
}
b.Publish(devices)
return nil
}
func (d *Driver) Run(ctx context.Context, bridge models.Bridge, ch chan<- models.Event) error {
b, err := d.ensureBridge(ctx, bridge)
if err != nil {
return err
}
return b.Run(ctx, bridge.Token == "debug")
}
func (d *Driver) ensureBridge(ctx context.Context, info models.Bridge) (*Bridge, error) {
d.mu.Lock()
for _, bridge := range d.bridges {
if bridge.ip == info.Address {
d.mu.Unlock()
return bridge, nil
}
}
d.mu.Unlock()
bridge := &Bridge{
ip: info.Address,
externalID: info.ID,
}
// Try to create a short-lived client to make sure it works.
ctx2, cancel := context.WithCancel(ctx)
defer cancel()
_, err := createClient(ctx2, info.Address, info.Token == "debug")
if err != nil {
return nil, err
}
// To avoid a potential duplicate, try looking for it again before inserting
d.mu.Lock()
for _, bridge := range d.bridges {
if bridge.ip == info.Address {
d.mu.Unlock()
return bridge, nil
}
}
d.bridges = append(d.bridges, bridge)
d.mu.Unlock()
return bridge, nil
}

191
internal/drivers/lifx/packet.go

@ -0,0 +1,191 @@
package lifx
import (
"encoding/binary"
"fmt"
"git.aiterp.net/lucifer/new-server/models"
"log"
"net"
)
func createPacket() Packet {
p := make(Packet, 36)
p.Reset()
return p
}
type Packet []byte
func (p *Packet) Reset() {
p.ensureHeader()
copy(*p, blankHeader)
}
func (p *Packet) Size() int {
return int(binary.LittleEndian.Uint16((*p)[0:2]))
}
func (p *Packet) Protocol() int {
return int(binary.LittleEndian.Uint16((*p)[2:3]) & 0b00001111_11111111)
}
func (p *Packet) SetTagged(v bool) {
if v {
(*p)[3] |= uint8(0b00100000)
} else {
(*p)[3] &= ^uint8(0b00100000)
}
}
func (p *Packet) Source() uint32 {
return binary.LittleEndian.Uint32((*p)[4:8])
}
func (p *Packet) Target() net.HardwareAddr {
return net.HardwareAddr((*p)[8:14])
}
func (p *Packet) SetSource(v uint32) {
binary.LittleEndian.PutUint32((*p)[4:8], v)
}
func (p *Packet) CopyTargetFrom(other Packet) {
copy((*p)[8:14], other[8:14])
}
func (p *Packet) SetTarget(v string) error {
addr, err := net.ParseMAC(v)
if err != nil {
return err
}
if len(addr) != 6 {
return models.ErrInvalidAddress
}
copy((*p)[8:], addr)
return nil
}
func (p *Packet) SetResRequired(v bool) {
if v {
(*p)[22] |= uint8(0b00000001)
} else {
(*p)[22] &= ^uint8(0b00000001)
}
}
func (p *Packet) SetAckRequired(v bool) {
if v {
(*p)[22] |= uint8(0b00000010)
} else {
(*p)[22] &= ^uint8(0b00000010)
}
}
func (p *Packet) Sequence() uint8 {
return (*p)[23]
}
func (p *Packet) SetSequence(v uint8) {
(*p)[23] = v
}
func (p *Packet) PacketType() uint16 {
return binary.LittleEndian.Uint16((*p)[32:34])
}
func (p *Packet) SetRawPayload(packetType uint16, data []byte) {
p.ensureSize(36 + len(data))
copy((*p)[36:], data)
binary.LittleEndian.PutUint16(*p, uint16(36+len(data)))
binary.LittleEndian.PutUint16((*p)[32:34], packetType)
}
func (p *Packet) Payload() (res Payload, err error) {
switch p.PacketType() {
case 2:
res = &GetService{}
case 3:
res = &StateService{}
err = res.Decode((*p)[36:])
case 14:
res = &GetHostFirmware{}
case 15:
res = &StateHostFirmware{}
err = res.Decode((*p)[36:])
case 32:
res = &GetVersion{}
case 33:
res = &StateVersion{}
err = res.Decode((*p)[36:])
case 45:
res = &Acknowledgement{}
case 101:
res = &GetColor{}
case 102:
res = &SetColor{}
err = res.Decode((*p)[36:])
case 107:
res = &LightState{}
err = res.Decode((*p)[36:])
case 117:
res = &SetLightPower{}
err = res.Decode((*p)[36:])
default:
err = models.ErrUnrecognizedPacketType
}
if err != nil {
res = nil
}
if res != nil && res.PacketType() != p.PacketType() {
log.Println("BUG Incorrect packet type used in Packet.Payload")
log.Println("BUG Payload Type:", p.PacketType())
log.Println("BUG Panic Packet Type:", res.PacketType())
log.Println("BUG Panic Payload:", res.String())
}
return
}
func (p *Packet) SetPayload(payload Payload) {
ackRequired, resRequired := payload.Flags()
p.SetRawPayload(payload.PacketType(), payload.Encode())
p.SetAckRequired(ackRequired)
p.SetResRequired(resRequired)
}
func (p *Packet) ensureHeader() {
p.ensureSize(36)
}
func (p *Packet) ensureSize(size int) {
if len(*p) < size {
newData := make([]byte, size)
copy(newData, *p)
*p = newData
}
}
type Payload interface {
fmt.Stringer
Decode(data []byte) error
Encode() []byte
PacketType() uint16
Flags() (ackRequired bool, resRequired bool)
}
var blankHeader = []byte{
0x24, 0x00, 0x00, 0x14,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
}

417
internal/drivers/lifx/payloads.go

@ -0,0 +1,417 @@
package lifx
import (
"encoding/binary"
"fmt"
"git.aiterp.net/lucifer/new-server/models"
"math"
"time"
)
type GetService struct{}
func (p *GetService) Decode([]byte) error {
return nil
}
func (p *GetService) Encode() []byte {
return []byte{}
}
func (p *GetService) PacketType() uint16 {
return 2
}
func (p *GetService) Flags() (ackRequired bool, resRequired bool) {
return false, true
}
func (p *GetService) String() string {
return "GetService()"
}
type GetVersion struct{}
func (p *GetVersion) Decode([]byte) error {
return nil
}
func (p *GetVersion) Encode() []byte {
return []byte{}
}
func (p *GetVersion) PacketType() uint16 {
return 32
}
func (p *GetVersion) Flags() (ackRequired bool, resRequired bool) {
return false, true
}
func (p *GetVersion) String() string {
return "GetVersion()"
}
type GetHostFirmware struct{}
func (p *GetHostFirmware) Decode([]byte) error {
return nil
}
func (p *GetHostFirmware) Encode() []byte {
return []byte{}
}
func (p *GetHostFirmware) PacketType() uint16 {
return 14
}
func (p *GetHostFirmware) Flags() (ackRequired bool, resRequired bool) {
return false, true
}
func (p *GetHostFirmware) String() string {
return "GetHostFirmware()"
}
type StateHostFirmware struct {
BuildTime time.Time
Major uint16
Minor uint16
}
func (p *StateHostFirmware) Decode(data []byte) error {
if len(data) < 20 {
return models.ErrPayloadTooShort
}
ts := int64(binary.LittleEndian.Uint64(data[0:8]))
p.BuildTime = time.Unix(ts/1000000000, ts%1000000000)
p.Major = binary.LittleEndian.Uint16(data[16:18])
p.Minor = binary.LittleEndian.Uint16(data[18:20])
return nil
}
func (p *StateHostFirmware) Encode() []byte {
var data [20]byte
binary.LittleEndian.PutUint64(data[0:8], uint64(p.BuildTime.UnixNano()))
binary.LittleEndian.PutUint16(data[16:18], p.Major)
binary.LittleEndian.PutUint16(data[18:20], p.Minor)
return data[:]
}
func (p *StateHostFirmware) PacketType() uint16 {
return 15
}
func (p *StateHostFirmware) Flags() (ackRequired bool, resRequired bool) {
return false, false
}
func (p *StateHostFirmware) String() string {
return fmt.Sprintf("StateHostFirmware(major=%d, minor=%d, build=%s)", p.Major, p.Minor, p.BuildTime.Format(time.RFC3339))
}
type StateVersion struct {
Vendor uint32
Product uint32
}
func (p *StateVersion) Decode(data []byte) error {
if len(data) < 8 {
return models.ErrPayloadTooShort
}
p.Vendor = binary.LittleEndian.Uint32(data[0:4])
p.Product = binary.LittleEndian.Uint32(data[4:8])
return nil
}
func (p *StateVersion) Encode() []byte {
var data [12]byte
binary.LittleEndian.PutUint32(data[0:4], p.Vendor)
binary.LittleEndian.PutUint32(data[4:8], p.Product)
return data[:]
}
func (p *StateVersion) PacketType() uint16 {
return 33
}
func (p *StateVersion) Flags() (ackRequired bool, resRequired bool) {
return false, false
}
func (p *StateVersion) String() string {
return fmt.Sprintf("StateVersion(vendor=%d, product=%d)", p.Vendor, p.Product)
}
type GetColor struct{}
func (p *GetColor) Decode([]byte) error {
return nil
}
func (p *GetColor) Encode() []byte {
return []byte{}
}
func (p *GetColor) PacketType() uint16 {
return 101
}
func (p *GetColor) Flags() (ackRequired bool, resRequired bool) {
return false, true
}
func (p *GetColor) String() string {
return "GetColor()"
}
type Acknowledgement struct{}
func (p *Acknowledgement) Decode([]byte) error {
return nil
}
func (p *Acknowledgement) Encode() []byte {
return []byte{}
}
func (p *Acknowledgement) PacketType() uint16 {
return 45
}
func (p *Acknowledgement) Flags() (ackRequired bool, resRequired bool) {
return false, true
}
func (p *Acknowledgement) String() string {
return "Acknowledgement()"
}
type SetColor struct {
Hue float64
Sat float64
Bri float64
Kelvin int
TransitionTime time.Duration
}
func (p *SetColor) Decode(data []byte) error {
if len(data) < 13 {
return models.ErrPayloadTooShort
}
hue := binary.LittleEndian.Uint16(data[1:3])
sat := binary.LittleEndian.Uint16(data[3:5])
bri := binary.LittleEndian.Uint16(data[5:7])
kelvin := binary.LittleEndian.Uint16(data[7:9])
transitionMs := binary.LittleEndian.Uint32(data[9:13])
p.Hue = float64(hue) / (65536 / 360)
p.Sat = float64(sat) / 65535
p.Bri = float64(bri) / 65535
p.Kelvin = int(kelvin)
p.TransitionTime = time.Duration(transitionMs) * time.Millisecond
return nil
}
func (p *SetColor) Encode() []byte {
var data [13]byte
data[0] = 0x00 // reserved
binary.LittleEndian.PutUint16(data[1:3], uint16(math.Mod(p.Hue, 360)*(65536/360)))
binary.LittleEndian.PutUint16(data[3:5], uint16(p.Sat*65535))
binary.LittleEndian.PutUint16(data[5:7], uint16(p.Bri*65535))
binary.LittleEndian.PutUint16(data[7:9], uint16(p.Kelvin))
binary.LittleEndian.PutUint32(data[9:13], uint32(p.TransitionTime.Milliseconds()))
return data[:]
}
func (p *SetColor) PacketType() uint16 {
return 102
}
func (p *SetColor) Flags() (ackRequired bool, resRequired bool) {
return true, false
}
func (p *SetColor) String() string {
return fmt.Sprintf("SetColor(hsvk={%f, %f, %f, %d}, ttime=%s)",
p.Hue,
p.Sat,
p.Bri,
p.Kelvin,
p.TransitionTime,
)
}
type SetLightPower struct {
On bool
TransitionTime time.Duration
}
func (p *SetLightPower) Decode(data []byte) error {
if len(data) < 6 {
return models.ErrPayloadTooShort
}
level := binary.LittleEndian.Uint16(data[0:2])
transitionMs := binary.LittleEndian.Uint32(data[2:6])
p.On = level > 0
p.TransitionTime = time.Duration(transitionMs) * time.Millisecond
return nil
}
func (p *SetLightPower) Encode() []byte {
var data [6]byte
if p.On {
data[0], data[1] = 0xff, 0xff
} else {
data[0], data[1] = 0x00, 0x00
}
binary.LittleEndian.PutUint32(data[2:6], uint32(p.TransitionTime.Milliseconds()))
return data[:]
}
func (p *SetLightPower) PacketType() uint16 {
return 117
}
func (p *SetLightPower) Flags() (ackRequired bool, resRequired bool) {
return true, false
}
func (p *SetLightPower) String() string {
return fmt.Sprintf("SetLightPower(on=%t, ttime=%s)",
p.On,
p.TransitionTime,
)
}
type StateService struct {
Service int
Port int
}
func (p *StateService) String() string {
return fmt.Sprintf("StateService(service=%d, port=%d)", p.Service, p.Port)
}
func (p *StateService) Decode(data []byte) error {
if len(data) < 5 {
return models.ErrPayloadTooShort
}
p.Service = int(data[0])
p.Port = int(binary.LittleEndian.Uint32(data[1:5]))
return nil
}
func (p *StateService) Encode() []byte {
var data [5]byte
data[0] = uint8(p.Service)
binary.LittleEndian.PutUint32(data[1:5], uint32(p.Port))
return data[:]
}
func (p *StateService) PacketType() uint16 {
return 3
}
func (p *StateService) Flags() (ackRequired bool, resRequired bool) {
return false, false
}
type LightState struct {
Hue float64
Sat float64
Bri float64
Kelvin int
On bool
Label string
}
func (p *LightState) String() string {
return fmt.Sprintf("LightState(hsvk=(%f, %f, %f, %d), on=%t, label=%+v)",
p.Hue, p.Sat, p.Bri, p.Kelvin,
p.On, p.Label,
)
}
func (p *LightState) Decode(data []byte) error {
if len(data) < 52 {
return models.ErrPayloadTooShort
}
hue := binary.LittleEndian.Uint16(data[0:2])
sat := binary.LittleEndian.Uint16(data[2:4])
bri := binary.LittleEndian.Uint16(data[4:6])
kelvin := binary.LittleEndian.Uint16(data[6:8])
power := binary.LittleEndian.Uint16(data[10:12])
p.Hue = float64(hue) / (65536 / 360)
p.Sat = float64(sat) / 65535
p.Bri = float64(bri) / 65535
p.Kelvin = int(kelvin)
p.On = power > 32767
p.Label = ""
for i, v := range data[14:46] {
if v == 0 {
p.Label = string(data[14 : 14+i])
break
}
}
return nil
}
func (p *LightState) Encode() []byte {
var data [52]byte
power := uint16(0)
if p.On {
power = 0xffff
}
binary.LittleEndian.PutUint16(data[0:2], uint16(math.Mod(p.Hue, 360)*(65536/360)))
binary.LittleEndian.PutUint16(data[2:4], uint16(p.Sat*65535))
binary.LittleEndian.PutUint16(data[4:6], uint16(p.Bri*65535))
binary.LittleEndian.PutUint16(data[6:8], uint16(p.Kelvin))
binary.LittleEndian.PutUint16(data[10:12], power)
label := p.Label
labelBytes := []byte(label)
for len(labelBytes) > 31 {
label = label[:len(label)-1]
labelBytes = []byte(label)
}
copy(data[14:], labelBytes)
return data[:]
}
func (p *LightState) PacketType() uint16 {
return 107
}
func (p *LightState) Flags() (ackRequired bool, resRequired bool) {
return false, false
}

73
internal/drivers/lifx/products.go
File diff suppressed because it is too large
View File

87
internal/drivers/lifx/state.go

@ -0,0 +1,87 @@
package lifx
import (
"git.aiterp.net/lucifer/new-server/models"
"math"
"time"
)
type State struct {
target string
externalId int
lightState *LightState
firmware *StateHostFirmware
version *StateVersion
deviceState *models.DeviceState
discoveredTime time.Time
lightStateTime time.Time
requestTime time.Time
updateTime time.Time
acksPending []uint8
}
func (s *State) generateUpdate() []Payload {
if s.deviceState == nil || s.lightState == nil {
return nil
}
results := make([]Payload, 0, 0)
if s.deviceState.Power != s.lightState.On {
results = append(results, &SetLightPower{On: s.deviceState.Power, TransitionTime: time.Millisecond * 100})
}
if !s.deviceState.Power {
return results
}
c := s.deviceState.Color
l := s.lightState
di := s.deviceState.Intensity
k := c.Kelvin
if k == 0 {
k = 4000
}
if !equalish(c.Hue, l.Hue) || !equalish(c.Saturation, l.Sat) || !equalish(di, l.Bri) || k != l.Kelvin {
results = append(results, &SetColor{
Hue: c.Hue,
Sat: c.Saturation,
Bri: di,
Kelvin: k,
TransitionTime: time.Millisecond * 150,
})
}
return results
}
func (s *State) handleAck(seq uint8) {
for i, pendingAck := range s.acksPending {
if pendingAck == seq {
s.acksPending = append(s.acksPending[:i], s.acksPending[i+1:]...)
break
}
}
if len(s.acksPending) == 0 {
s.lightStateTime = time.Now()
prevLabel := ""
if s.lightState != nil {
prevLabel = s.lightState.Label
}
s.lightState = &LightState{
Hue: s.deviceState.Color.Hue,
Sat: s.deviceState.Color.Saturation,
Bri: s.deviceState.Intensity,
Kelvin: s.deviceState.Color.Kelvin,
On: s.deviceState.Power,
Label: prevLabel,
}
}
}
func equalish(a, b float64) bool {
return math.Abs(a-b) < 0.005
}

1
models/bridge.go

@ -22,6 +22,7 @@ type DriverKind string
var ( var (
DTHue DriverKind = "Hue" DTHue DriverKind = "Hue"
DTNanoLeaf DriverKind = "Nanoleaf" DTNanoLeaf DriverKind = "Nanoleaf"
DTLIFX DriverKind = "LIFX"
) )
var ValidDriverKinds = []DriverKind{ var ValidDriverKinds = []DriverKind{

22
models/colorvalue.go

@ -35,7 +35,7 @@ func ParseColorValue(raw string) (ColorValue, error) {
return ColorValue{}, ErrBadInput return ColorValue{}, ErrBadInput
} }
if tokens[0] == "kelvin" {
if tokens[0] == "kelvin" || tokens[0] == "k" {
parsedPart, err := strconv.Atoi(tokens[1]) parsedPart, err := strconv.Atoi(tokens[1])
if err != nil { if err != nil {
return ColorValue{}, ErrBadInput return ColorValue{}, ErrBadInput
@ -59,5 +59,25 @@ func ParseColorValue(raw string) (ColorValue, error) {
return ColorValue{Hue: math.Mod(part1, 360), Saturation: math.Min(math.Max(part2, 0), 1)}, nil return ColorValue{Hue: math.Mod(part1, 360), Saturation: math.Min(math.Max(part2, 0), 1)}, nil
} }
if tokens[0] == "hsk" {
parts := strings.Split(tokens[1], ",")
if len(parts) < 3 {
return ColorValue{}, ErrUnknownColorFormat
}
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 {
return ColorValue{}, ErrBadInput
}
return ColorValue{
Hue: math.Mod(part1, 360),
Saturation: math.Min(math.Max(part2, 0), 1),
Kelvin: part3,
}, nil
}
return ColorValue{}, ErrUnknownColorFormat return ColorValue{}, ErrUnknownColorFormat
} }

11
models/device.go

@ -50,6 +50,7 @@ type DeviceRepository interface {
var ( var (
DCPower DeviceCapability = "Power" DCPower DeviceCapability = "Power"
DCColorHS DeviceCapability = "ColorHS" DCColorHS DeviceCapability = "ColorHS"
DCColorHSK DeviceCapability = "ColorHSK"
DCColorKelvin DeviceCapability = "ColorKelvin" DCColorKelvin DeviceCapability = "ColorKelvin"
DCButtons DeviceCapability = "Buttons" DCButtons DeviceCapability = "Buttons"
DCPresence DeviceCapability = "Presence" DCPresence DeviceCapability = "Presence"
@ -89,10 +90,12 @@ func (d *Device) Validate() error {
return nil return nil
} }
func (d *Device) HasCapability(capacity DeviceCapability) bool {
func (d *Device) HasCapability(capabilities ...DeviceCapability) bool {
for _, c := range d.Capabilities { for _, c := range d.Capabilities {
if c == capacity {
return true
for _, c2 := range capabilities {
if c == c2 {
return true
}
} }
} }
@ -110,7 +113,7 @@ func (d *Device) SetState(newState NewDeviceState) error {
return err return err
} }
if (parsed.IsKelvin() && d.HasCapability(DCColorKelvin)) || (parsed.IsHueSat() && d.HasCapability(DCColorHS)) {
if (parsed.IsKelvin() && d.HasCapability(DCColorKelvin, DCColorHSK)) || (parsed.IsHueSat() && d.HasCapability(DCColorHS)) {
d.State.Color = parsed d.State.Color = parsed
} }
} }

7
models/errors.go

@ -15,3 +15,10 @@ var ErrIncorrectToken = errors.New("driver is not accepting authentication infor
var ErrUnexpectedResponse = errors.New("driver api returned unexpected response (wrong driver selected?)") var ErrUnexpectedResponse = errors.New("driver api returned unexpected response (wrong driver selected?)")
var ErrBridgeSearchFailed = errors.New("bridge search failed") var ErrBridgeSearchFailed = errors.New("bridge search failed")
var ErrAddressOnlyDryRunnable = errors.New("this address may only be used for a dry run") var ErrAddressOnlyDryRunnable = errors.New("this address may only be used for a dry run")
var ErrInvalidAddress = errors.New("invalid mac address")
var ErrPayloadTooShort = errors.New("payload too short")
var ErrInvalidPacketSize = errors.New("invalid packet size")
var ErrReadTimeout = errors.New("read timeout")
var ErrUnrecognizedPacketType = errors.New("packet type not recognized")
var ErrBridgeRunningRequired = errors.New("this operation cannot be performed when bridge is not running")
Loading…
Cancel
Save