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
-
9models/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