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.
 
 
 
 

399 lines
8.3 KiB

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 "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
}