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.
269 lines
6.6 KiB
269 lines
6.6 KiB
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
|
|
}
|