|
|
package lifx
import ( "context" "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 models.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 == 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 && 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 }
|