|
|
package hue
import ( "context" "encoding/json" "encoding/xml" "fmt" "git.aiterp.net/lucifer/new-server/models" "log" "net/http" "strconv" "sync" "time" )
type Driver struct { mu sync.Mutex bridges []*Bridge }
func (d *Driver) SearchBridge(ctx context.Context, address string, dryRun bool) ([]models.Bridge, error) { if address == "" { if !dryRun { return nil, models.ErrAddressOnlyDryRunnable }
res, err := http.Get("https://discovery.meethue.com") if err != nil { return nil, err } defer res.Body.Close()
entries := make([]DiscoveryEntry, 0, 8) err = json.NewDecoder(res.Body).Decode(&entries) if err != nil { return nil, err }
bridges := make([]models.Bridge, 0, len(entries)) for _, entry := range entries { bridges = append(bridges, models.Bridge{ ID: -1, Name: entry.Id, Driver: models.DTHue, Address: entry.InternalIPAddress, Token: "", }) }
return bridges, nil }
deviceInfo := BridgeDeviceInfo{} res, err := http.Get(fmt.Sprintf("http://%s/description.xml", address)) if err != nil { return nil, err } defer res.Body.Close() err = xml.NewDecoder(res.Body).Decode(&deviceInfo) if err != nil { return nil, err }
bridge := models.Bridge{ ID: -1, Name: deviceInfo.Device.FriendlyName, Driver: models.DTHue, Address: address, Token: "", }
if !dryRun { b := &Bridge{host: address}
timeout, cancel := context.WithTimeout(ctx, time.Second*30) defer cancel()
ticker := time.NewTicker(time.Second) defer ticker.Stop()
for range ticker.C { token, err := b.getToken(timeout) if err != nil { if err == errLinkButtonNotPressed { continue }
return nil, err }
bridge.Token = token b.token = token break } }
return []models.Bridge{bridge}, 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 }
if timeout.Seconds() < 10 { timeout = time.Second * 10 } halfTime := timeout / 2
err = b.StartDiscovery(ctx, "sensors") if err != nil { return nil, err } select { case <-time.After(halfTime): case <-ctx.Done(): return nil, ctx.Err() }
err = b.StartDiscovery(ctx, "lights") if err != nil { return nil, err } select { case <-time.After(halfTime): case <-ctx.Done(): return nil, ctx.Err() }
err = b.Refresh(ctx) if err != nil { return nil, err }
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 }
devices := make([]models.Device, 0, 8)
lightMap, err := b.getLights(ctx) if err != nil { return nil, err } for _, lightInfo := range lightMap { device := models.Device{ ID: -1, BridgeID: b.externalID, InternalID: lightInfo.Uniqueid, Icon: "lightbulb", Name: lightInfo.Name, Capabilities: []models.DeviceCapability{ models.DCPower, }, ButtonNames: nil, DriverProperties: map[string]interface{}{ "modelId": lightInfo.Modelid, "productName": lightInfo.Productname, "swVersion": lightInfo.Swversion, "hueLightType": lightInfo.Type, }, UserProperties: nil, State: models.DeviceState{}, Tags: nil, }
hasDimming := false hasCT := false hasColor := false
switch lightInfo.Type { case "On/off light": // Always take DCPower for granted anyway.
case "Dimmable light": hasDimming = true case "Color temperature light": hasDimming = true hasCT = true case "Color light": hasDimming = true hasColor = true case "Extended color light": hasDimming = true hasColor = true hasCT = true }
ctrl := lightInfo.Capabilities.Control if hasDimming { device.Capabilities = append(device.Capabilities, models.DCIntensity) } if hasCT { device.Capabilities = append(device.Capabilities, models.DCColorKelvin) device.DriverProperties["minKelvin"] = 1000000 / ctrl.CT.Max device.DriverProperties["maxKelvin"] = 1000000 / ctrl.CT.Min } if hasColor { device.Capabilities = append(device.Capabilities, models.DCColorHS) device.DriverProperties["gamutType"] = ctrl.Colorgamuttype device.DriverProperties["gamutData"] = ctrl.Colorgamut } device.DriverProperties["maxLumen"] = strconv.Itoa(ctrl.Maxlumen)
devices = append(devices, device) }
sensorMap, err := b.getSensors(ctx) if err != nil { return nil, err } for _, sensorInfo := range sensorMap { device := models.Device{ ID: -1, BridgeID: b.externalID, InternalID: sensorInfo.UniqueID, Name: sensorInfo.Name, Capabilities: []models.DeviceCapability{}, ButtonNames: []string{}, DriverProperties: map[string]interface{}{ "modelId": sensorInfo.Modelid, "productName": sensorInfo.Productname, "swVersion": sensorInfo.Swversion, "hueLightType": sensorInfo.Type, }, UserProperties: nil, State: models.DeviceState{}, Tags: nil, }
switch sensorInfo.Type { case "ZLLSwitch": device.Capabilities = append(device.Capabilities, models.DCButtons) device.ButtonNames = append(buttonNames[:0:0], buttonNames...) device.Icon = "lightswitch" case "ZLLPresence": device.Capabilities = append(device.Capabilities, models.DCPresence) device.Icon = "sensor" case "ZLLTemperature": device.Capabilities = append(device.Capabilities, models.DCTemperatureSensor) device.Icon = "thermometer" case "Daylight": continue }
devices = append(devices, device) }
return 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 }
err = b.Refresh(ctx) if err != nil { return err }
b.mu.Lock() for _, device := range devices { for _, state := range b.lightStates { if device.InternalID == state.uniqueID { state.externalID = device.ID state.Update(device.State) break } }
for _, state := range b.sensorStates { if device.InternalID == state.uniqueID { state.externalID = device.ID break } } } b.mu.Unlock()
return b.SyncStale(ctx) }
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 }
fastTicker := time.NewTicker(time.Second / 10) slowTicker := time.NewTicker(time.Second / 3) selectedTicker := fastTicker ticksUntilRefresh := 0 ticksSinceChange := 0
for { select { case <-selectedTicker.C: if ticksUntilRefresh <= 0 { err := b.Refresh(ctx) if err != nil { return err } err = b.SyncStale(ctx) if err != nil { return err }
ticksUntilRefresh = 60 }
events, err := b.SyncSensors(ctx) if err != nil { return err }
for _, event := range events { ch <- event ticksSinceChange = 0 }
if ticksSinceChange > 30 { selectedTicker = slowTicker } else if ticksSinceChange == 0 { selectedTicker = fastTicker } case <-ctx.Done(): return ctx.Err() }
ticksUntilRefresh -= 1 ticksSinceChange += 1 } }
func (d *Driver) ensureBridge(ctx context.Context, info models.Bridge) (*Bridge, error) { d.mu.Lock() for _, bridge := range d.bridges { if bridge.host == info.Address { d.mu.Unlock() return bridge, nil } } d.mu.Unlock()
bridge := &Bridge{ host: info.Address, token: info.Token, externalID: info.ID, }
// If this call succeeds, then the token is ok.
lightMap, err := bridge.getLights(ctx) if err != nil { return nil, err } log.Printf("Found %d lights on bridge %d", len(lightMap), bridge.externalID)
// To avoid a potential duplicate, try looking for it again before inserting
d.mu.Lock() for _, bridge := range d.bridges { if bridge.host == info.Address { d.mu.Unlock() return bridge, nil } } d.bridges = append(d.bridges, bridge) d.mu.Unlock()
return bridge, nil }
|