You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

273 lines
6.8 KiB

package lifx
import (
"context"
"git.aiterp.net/lucifer/new-server/internal/color"
"git.aiterp.net/lucifer/new-server/internal/lerrors"
"git.aiterp.net/lucifer/new-server/models"
"log"
"sync"
"sync/atomic"
"time"
)
type Bridge struct {
mu sync.Mutex
externalID int
ip string
states []*State
client *Client
}
func (b *Bridge) StartSearch(ctx context.Context) error {
c := b.getClient()
if c == nil {
return lerrors.ErrBridgeRunningRequired
}
_, err := c.HorribleBroadcast(ctx, &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 == lerrors.ErrInvalidPacketSize || err == lerrors.ErrPayloadTooShort || err == lerrors.ErrUnrecognizedPacketType {
log.Println("LIFX udp socket received something weird:", err)
} else if err != nil && err != lerrors.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: color.Color{
HS: &color.HueSat{
Hue: p.Hue,
Sat: p.Sat,
},
K: &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 && time.Since(state.fwSpamTime) > time.Second*30 {
state.fwSpamTime = time.Now()
if state.firmware == nil {
_, _ = client.Send(state.target, &GetHostFirmware{})
}
if state.version == nil {
_, _ = client.Send(state.target, &GetVersion{})
}
}
}
b.mu.Unlock()
if atomic.LoadUint32(&client.isHorrible) == 0 && 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
}