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 }