add lifx driver. It works with the bulb, but the code is a bit smelly.
This commit is contained in:
@@ -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{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+37
-8
@@ -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))
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -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
|
||||||
|
}
|
||||||
@@ -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{
|
||||||
|
|||||||
+21
-1
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-4
@@ -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 {
|
for _, c2 := range capabilities {
|
||||||
return true
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user