Gisle Aune
4 years ago
13 changed files with 1465 additions and 13 deletions
-
2app/config/driver.go
-
45cmd/bridgetest/main.go
-
267internal/drivers/lifx/bridge.go
-
178internal/drivers/lifx/client.go
-
177internal/drivers/lifx/driver.go
-
191internal/drivers/lifx/packet.go
-
417internal/drivers/lifx/payloads.go
-
73internal/drivers/lifx/products.go
-
87internal/drivers/lifx/state.go
-
1models/bridge.go
-
22models/colorvalue.go
-
11models/device.go
-
7models/errors.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 |
|||
} |
@ -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 |
|||
} |
73
internal/drivers/lifx/products.go
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -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 |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue