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.
257 lines
6.7 KiB
257 lines
6.7 KiB
package hue2
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"git.aiterp.net/lucifer/new-server/models"
|
|
"golang.org/x/sync/errgroup"
|
|
"math"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type Bridge struct {
|
|
mu sync.Mutex
|
|
client *Client
|
|
devices map[string]models.Device
|
|
resources map[string]*ResourceData
|
|
}
|
|
|
|
func NewBridge(client *Client) *Bridge {
|
|
return &Bridge{
|
|
client: client,
|
|
devices: make(map[string]models.Device, 64),
|
|
resources: make(map[string]*ResourceData, 256),
|
|
}
|
|
}
|
|
|
|
func (b *Bridge) Update(devices ...models.Device) {
|
|
b.mu.Lock()
|
|
for _, device := range devices {
|
|
b.devices[device.InternalID] = device
|
|
}
|
|
b.mu.Unlock()
|
|
}
|
|
|
|
func (b *Bridge) MakeCongruent(ctx context.Context) (int, error) {
|
|
b.mu.Lock()
|
|
dur := time.Millisecond * 200
|
|
updates := make(map[string]ResourceUpdate)
|
|
for _, device := range b.devices {
|
|
resource := b.resources[device.InternalID]
|
|
if lightID := resource.ServiceID("light"); lightID != nil {
|
|
light := b.resources[*lightID]
|
|
update := ResourceUpdate{TransitionDuration: &dur}
|
|
changed := false
|
|
|
|
if light.ColorTemperature != nil && device.State.Color.IsKelvin() {
|
|
mirek := 1000000 / device.State.Color.Kelvin
|
|
if mirek < light.ColorTemperature.MirekSchema.MirekMinimum {
|
|
mirek = light.ColorTemperature.MirekSchema.MirekMinimum
|
|
}
|
|
if mirek > light.ColorTemperature.MirekSchema.MirekMaximum {
|
|
mirek = light.ColorTemperature.MirekSchema.MirekMaximum
|
|
}
|
|
if light.ColorTemperature.Mirek == nil || mirek != *light.ColorTemperature.Mirek {
|
|
update.Mirek = &mirek
|
|
changed = true
|
|
}
|
|
} else if xy, ok := device.State.Color.ToXY(); ok && light.Color != nil {
|
|
xy = light.Color.Gamut.Conform(xy).Round()
|
|
if xy.DistanceTo(light.Color.XY) > 0.00015 || (light.ColorTemperature != nil && light.ColorTemperature.Mirek != nil) {
|
|
update.ColorXY = &xy
|
|
changed = true
|
|
}
|
|
}
|
|
if light.Power != nil && light.Power.On != device.State.Power {
|
|
update.Power = &device.State.Power
|
|
changed = true
|
|
}
|
|
if light.Dimming != nil && math.Abs(light.Dimming.Brightness/100-device.State.Intensity) > 0.005 {
|
|
brightness := math.Abs(math.Min(device.State.Intensity*100, 100))
|
|
update.Brightness = &brightness
|
|
changed = true
|
|
}
|
|
|
|
if changed {
|
|
updates["light/"+light.ID] = update
|
|
}
|
|
}
|
|
}
|
|
b.mu.Unlock()
|
|
|
|
if len(updates) == 0 {
|
|
return 0, nil
|
|
}
|
|
|
|
eg, ctx := errgroup.WithContext(ctx)
|
|
for key := range updates {
|
|
update := updates[key]
|
|
split := strings.SplitN(key, "/", 2)
|
|
link := ResourceLink{Kind: split[0], ID: split[1]}
|
|
eg.Go(func() error {
|
|
return b.client.UpdateResource(ctx, link, update)
|
|
})
|
|
}
|
|
|
|
return len(updates), eg.Wait()
|
|
}
|
|
|
|
func (b *Bridge) GenerateDevices() []models.Device {
|
|
b.mu.Lock()
|
|
resources := b.resources
|
|
b.mu.Unlock()
|
|
|
|
devices := make([]models.Device, 0, 16)
|
|
for _, resource := range resources {
|
|
if resource.Type != "device" || strings.HasPrefix(resource.Metadata.Archetype, "bridge") {
|
|
continue
|
|
}
|
|
|
|
device := models.Device{
|
|
InternalID: resource.ID,
|
|
Name: resource.Metadata.Name,
|
|
DriverProperties: map[string]interface{}{
|
|
"archetype": resource.Metadata.Archetype,
|
|
"name": resource.ProductData.ProductName,
|
|
"product": resource.ProductData,
|
|
"legacyId": resource.LegacyID,
|
|
},
|
|
}
|
|
|
|
// Set icon
|
|
if resource.ProductData.ProductName == "Hue dimmer switch" {
|
|
device.Icon = "switch"
|
|
} else if resource.ProductData.ProductName == "Hue motion sensor" {
|
|
device.Icon = "sensor"
|
|
} else {
|
|
device.Icon = "lightbulb"
|
|
}
|
|
|
|
buttonCount := 0
|
|
for _, ptr := range resource.Services {
|
|
switch ptr.Kind {
|
|
case "device_power":
|
|
{
|
|
device.DriverProperties["battery"] = resources[ptr.ID].PowerState
|
|
}
|
|
case "button":
|
|
{
|
|
buttonCount += 1
|
|
}
|
|
case "zigbee_connectivity":
|
|
{
|
|
device.DriverProperties["zigbee"] = resources[ptr.ID].Status
|
|
}
|
|
case "motion":
|
|
{
|
|
device.Capabilities = append(device.Capabilities, models.DCPresence)
|
|
}
|
|
case "temperature":
|
|
{
|
|
device.Capabilities = append(device.Capabilities, models.DCTemperatureSensor)
|
|
}
|
|
case "light":
|
|
{
|
|
light := resources[ptr.ID]
|
|
|
|
if light.Power != nil {
|
|
device.State.Power = light.Power.On
|
|
device.Capabilities = append(device.Capabilities, models.DCPower)
|
|
}
|
|
if light.Dimming != nil {
|
|
device.State.Intensity = light.Dimming.Brightness / 100
|
|
device.Capabilities = append(device.Capabilities, models.DCIntensity)
|
|
}
|
|
if light.ColorTemperature != nil {
|
|
if light.ColorTemperature.Mirek != nil {
|
|
device.State.Color = models.ColorValue{
|
|
Kelvin: int(1000000 / *light.ColorTemperature.Mirek),
|
|
}
|
|
}
|
|
device.Capabilities = append(device.Capabilities, models.DCColorKelvin)
|
|
device.DriverProperties["maxTemperature"] = 1000000 / light.ColorTemperature.MirekSchema.MirekMinimum
|
|
device.DriverProperties["minTemperature"] = 1000000 / light.ColorTemperature.MirekSchema.MirekMaximum
|
|
}
|
|
if light.Color != nil {
|
|
if device.State.Color.IsEmpty() {
|
|
device.State.Color = models.ColorValue{
|
|
XY: &light.Color.XY,
|
|
}
|
|
}
|
|
device.DriverProperties["colorGamut"] = light.Color.Gamut
|
|
device.DriverProperties["colorGamutType"] = light.Color.GamutType
|
|
device.Capabilities = append(device.Capabilities, models.DCColorHS, models.DCColorXY)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if buttonCount == 4 {
|
|
device.ButtonNames = []string{"On", "DimUp", "DimDown", "Off"}
|
|
} else if buttonCount == 1 {
|
|
device.ButtonNames = []string{"Button"}
|
|
} else {
|
|
for n := 1; n <= buttonCount; n++ {
|
|
device.ButtonNames = append(device.ButtonNames, fmt.Sprint("Button", n))
|
|
}
|
|
}
|
|
|
|
devices = append(devices, device)
|
|
}
|
|
|
|
return devices
|
|
}
|
|
|
|
func (b *Bridge) Refresh(ctx context.Context, kind string) error {
|
|
if kind == "device" {
|
|
// Device refresh requires the full deal as services are taken for granted.
|
|
return b.RefreshAll(ctx)
|
|
}
|
|
|
|
resources, err := b.client.Resources(ctx, kind)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
b.mu.Lock()
|
|
oldResources := b.resources
|
|
b.mu.Unlock()
|
|
|
|
newResources := make(map[string]*ResourceData, len(b.resources))
|
|
for key, value := range oldResources {
|
|
if value.Type != kind {
|
|
newResources[key] = value
|
|
}
|
|
}
|
|
for i := range resources {
|
|
resource := resources[i]
|
|
newResources[resource.ID] = &resource
|
|
}
|
|
|
|
b.mu.Lock()
|
|
b.resources = newResources
|
|
b.mu.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *Bridge) RefreshAll(ctx context.Context) error {
|
|
allResources, err := b.client.AllResources(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resources := make(map[string]*ResourceData, len(allResources))
|
|
|
|
for i := range allResources {
|
|
resource := allResources[i]
|
|
resources[resource.ID] = &resource
|
|
}
|
|
|
|
b.mu.Lock()
|
|
b.resources = resources
|
|
b.mu.Unlock()
|
|
|
|
return nil
|
|
}
|