hue driver can set stuff now, I hope

This commit is contained in:
2022-12-29 16:31:30 +01:00
parent 6f8def066f
commit edbc576eed
9 changed files with 420 additions and 23 deletions
+234 -16
View File
@@ -6,8 +6,11 @@ import (
lucifer3 "git.aiterp.net/lucifer3/server"
"git.aiterp.net/lucifer3/server/device"
"git.aiterp.net/lucifer3/server/events"
"git.aiterp.net/lucifer3/server/internal/color"
"git.aiterp.net/lucifer3/server/internal/gentools"
"golang.org/x/sync/errgroup"
"log"
"math"
"strings"
"sync"
"time"
@@ -15,14 +18,16 @@ import (
func NewBridge(host string, client *Client) *Bridge {
return &Bridge{
client: client,
host: host,
ctx: context.Background(),
cancel: func() {},
resources: map[string]*ResourceData{},
activeStates: map[string]device.State{},
desiredStates: map[string]device.State{},
hasSeen: map[string]bool{},
client: client,
host: host,
ctx: context.Background(),
cancel: func() {},
resources: map[string]*ResourceData{},
activeStates: map[string]device.State{},
desiredStates: map[string]device.State{},
colorFlags: map[string]device.ColorFlags{},
hasSeen: map[string]bool{},
triggerCongruenceCheckCh: make(chan struct{}, 2),
}
}
@@ -37,8 +42,12 @@ type Bridge struct {
resources map[string]*ResourceData
activeStates map[string]device.State
desiredStates map[string]device.State
colorFlags map[string]device.ColorFlags
reachable map[string]bool
hasSeen map[string]bool
triggerCongruenceCheckCh chan struct{}
lastDiscoverCancel context.CancelFunc
}
@@ -100,20 +109,39 @@ func (b *Bridge) RefreshAll() ([]lucifer3.Event, error) {
resources[allResources[i].ID] = &allResources[i]
}
newEvents := make([]lucifer3.Event, 0, 0)
b.mu.Lock()
hasSeen := b.hasSeen
reachable := b.reachable
b.mu.Unlock()
hasSeen = gentools.CopyMap(hasSeen)
reachable = gentools.CopyMap(reachable)
colorFlags := make(map[string]device.ColorFlags)
activeStates := make(map[string]device.State)
newEvents := make([]lucifer3.Event, 0, 0)
for id, res := range resources {
if res.Type == "device" {
if res.Type == "device" && res.Metadata.Archetype != "bridge_v2" {
hwState, hwEvent := res.GenerateEvent(b.host, resources)
if !b.hasSeen[id] {
if !hasSeen[id] {
newEvents = append(newEvents, hwState, hwEvent, events.DeviceReady{ID: hwState.ID})
b.hasSeen[id] = true
hasSeen[id] = true
} else {
newEvents = append(newEvents, hwState)
}
activeStates[id] = hwState.State
colorFlags[id] = hwState.ColorFlags
reachable[id] = !hwState.Unreachable
}
}
b.mu.Lock()
b.resources = resources
b.hasSeen = hasSeen
b.colorFlags = colorFlags
b.activeStates = activeStates
b.reachable = reachable
b.mu.Unlock()
return newEvents, nil
@@ -121,9 +149,17 @@ func (b *Bridge) RefreshAll() ([]lucifer3.Event, error) {
func (b *Bridge) ApplyPatches(resources []ResourceData) (events []lucifer3.Event, shouldRefresh bool) {
b.mu.Lock()
mapCopy := gentools.CopyMap(b.resources)
resourceMap := b.resources
activeStates := b.activeStates
reachable := b.reachable
colorFlags := b.colorFlags
b.mu.Unlock()
mapCopy := gentools.CopyMap(resourceMap)
activeStatesCopy := gentools.CopyMap(activeStates)
reachableCopy := gentools.CopyMap(reachable)
colorFlagsCopy := gentools.CopyMap(colorFlags)
for _, resource := range resources {
if mapCopy[resource.ID] != nil {
mapCopy[resource.ID] = mapCopy[resource.ID].WithPatch(resource)
@@ -138,12 +174,21 @@ func (b *Bridge) ApplyPatches(resources []ResourceData) (events []lucifer3.Event
if parent, ok := mapCopy[resource.Owner.ID]; ok {
hwState, _ := parent.GenerateEvent(b.host, mapCopy)
events = append(events, hwState)
activeStatesCopy[resource.Owner.ID] = hwState.State
reachableCopy[resource.Owner.ID] = !hwState.Unreachable
if hwState.ColorFlags != 0 {
colorFlagsCopy[resource.Owner.ID] = hwState.ColorFlags
}
}
}
}
b.mu.Lock()
b.resources = mapCopy
b.activeStates = activeStatesCopy
b.reachable = reachableCopy
b.colorFlags = colorFlagsCopy
b.mu.Unlock()
return
@@ -167,20 +212,28 @@ func (b *Bridge) SetStates(patch map[string]device.State) {
continue
}
newStates[id] = resource.FixState(state, resources)
if !state.Empty() {
newStates[id] = resource.FixState(state, resources)
} else {
delete(newStates, id)
}
}
b.mu.Lock()
b.desiredStates = newStates
b.mu.Unlock()
b.triggerCongruenceCheck()
}
func (b *Bridge) Run(ctx context.Context, bus *lucifer3.EventBus) interface{} {
hwEvents, err := b.RefreshAll()
if err != nil {
return nil
return errors.New("failed to connect to bridge")
}
go b.makeCongruentLoop(ctx)
bus.RunEvents(hwEvents)
sse := b.client.SSE(ctx)
@@ -206,17 +259,182 @@ func (b *Bridge) Run(ctx context.Context, bus *lucifer3.EventBus) interface{} {
if shouldUpdate {
hwEvents, err := b.RefreshAll()
if err != nil {
return nil
return errors.New("failed to refresh states")
}
bus.RunEvents(hwEvents)
}
b.triggerCongruenceCheck()
}
case <-step.C:
hwEvents, err := b.RefreshAll()
if err != nil {
return nil
}
bus.RunEvents(hwEvents)
b.triggerCongruenceCheck()
case <-ctx.Done():
{
return nil
}
}
}
}
func (b *Bridge) makeCongruentLoop(ctx context.Context) {
for range b.triggerCongruenceCheckCh {
if ctx.Err() != nil {
break
}
// Make sure this loop takes half a second
rateLimit := time.After(time.Second / 2)
// Take states
b.mu.Lock()
resources := b.resources
desiredStates := b.desiredStates
activeStates := b.activeStates
reachable := b.reachable
colorFlags := b.colorFlags
b.mu.Unlock()
newActiveStates := make(map[string]device.State, 0)
updates := make(map[string]ResourceUpdate)
for id, desired := range desiredStates {
active, activeOK := activeStates[id]
lightID := resources[id].ServiceID("light")
if !reachable[id] || !activeOK || lightID == nil {
log.Println("No light", !reachable[id], !activeOK, lightID == nil)
continue
}
light := resources[*lightID]
if light == nil {
log.Println("No light", *lightID)
continue
}
// Handle power first
if desired.Power != nil && active.Power != nil && *desired.Power != *active.Power {
updates["light/"+*lightID] = ResourceUpdate{Power: gentools.Ptr(*active.Power)}
newActiveState := activeStates[id]
newActiveState.Power = gentools.Ptr(*desired.Power)
newActiveStates[id] = newActiveState
continue
}
if active.Power != nil && *active.Power == false {
// Don't do more with shut-off-light.
continue
}
updated := false
update := ResourceUpdate{}
newActiveState := activeStates[id]
if active.Color != nil && desired.Color != nil {
ac := *active.Color
dc := *desired.Color
if !dc.IsKelvin() || !colorFlags[id].IsWarmWhite() {
dc, _ = dc.ToXY()
dc.XY = gentools.Ptr(light.Color.Gamut.Conform(*dc.XY))
}
if dc.XY != nil {
if ac.XY != nil {
dist := math.Abs(ac.XY.X-dc.XY.X) + math.Abs(ac.XY.Y-dc.XY.Y)
if dist > 0.0002 {
update.ColorXY = gentools.Ptr(*dc.XY)
updated = true
}
} else {
newActiveState.Color = &color.Color{XY: gentools.Ptr(*dc.XY)}
update.ColorXY = gentools.Ptr(*dc.XY)
updated = true
}
} else {
dcMirek := 1000000 / *dc.K
if dcMirek < light.ColorTemperature.MirekSchema.MirekMinimum {
dcMirek = light.ColorTemperature.MirekSchema.MirekMinimum
} else if dcMirek > light.ColorTemperature.MirekSchema.MirekMaximum {
dcMirek = light.ColorTemperature.MirekSchema.MirekMaximum
}
acMirek := 0
if ac.K != nil {
acMirek = 1000000 / *ac.K
}
if acMirek != dcMirek {
newActiveState.Color = &color.Color{K: gentools.Ptr(*dc.K)}
update.Mirek = &dcMirek
updated = true
}
}
}
if active.Intensity != nil && desired.Intensity != nil {
if math.Abs(*active.Intensity-*desired.Intensity) >= 0.01 {
update.Brightness = gentools.Ptr(*desired.Intensity * 100)
newActiveState.Intensity = gentools.Ptr(*desired.Intensity)
updated = true
}
}
if updated {
update.TransitionDuration = gentools.Ptr(time.Millisecond * 101)
updates["light/"+*lightID] = update
newActiveStates[id] = newActiveState
}
}
if len(updates) > 0 {
timeout, cancel := context.WithTimeout(ctx, time.Second)
eg, ctx := errgroup.WithContext(timeout)
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)
})
}
err := eg.Wait()
if err != nil {
log.Println("Failed to run update", err)
}
b.mu.Lock()
activeStates = b.activeStates
b.mu.Unlock()
activeStates = gentools.CopyMap(activeStates)
for id, state := range newActiveStates {
activeStates[id] = state
}
b.mu.Lock()
b.activeStates = activeStates
b.mu.Unlock()
cancel()
}
// Wait the remaining time for the rate limit
<-rateLimit
}
}
func (b *Bridge) triggerCongruenceCheck() {
select {
case b.triggerCongruenceCheckCh <- struct{}{}:
default:
}
}
+10 -5
View File
@@ -182,8 +182,6 @@ func (res *ResourceData) WithPatch(patch ResourceData) *ResourceData {
gentools.ShallowCopyTo(&res2.ProductData, patch.ProductData)
gentools.ShallowCopyTo(&res2.Button, patch.Button)
gentools.ShallowCopyTo(&res2.Power, patch.Power)
gentools.ShallowCopyTo(&res2.Color, patch.Color)
gentools.ShallowCopyTo(&res2.ColorTemperature, patch.ColorTemperature)
gentools.ShallowCopyTo(&res2.Dimming, patch.Dimming)
gentools.ShallowCopyTo(&res2.Dynamics, patch.Dynamics)
gentools.ShallowCopyTo(&res2.Alert, patch.Alert)
@@ -218,6 +216,11 @@ func (res *ResourceData) GenerateEvent(hostname string, resources map[string]*Re
}
switch ptr.Kind {
case "zigbee_connectivity":
switch *svc.Status {
case "connectivity_issue":
hwState.Unreachable = true
}
case "device_power":
hwState.BatteryPercentage = gentools.Ptr(int(svc.PowerState.BatteryLevel))
case "button":
@@ -237,9 +240,11 @@ func (res *ResourceData) GenerateEvent(hostname string, resources map[string]*Re
if svc.ColorTemperature != nil {
hwState.SupportFlags |= device.SFlagColor
hwState.ColorFlags |= device.CFlagKelvin
hwState.TemperatureRange = &[2]int{
1000000 / svc.ColorTemperature.MirekSchema.MirekMinimum,
1000000 / svc.ColorTemperature.MirekSchema.MirekMaximum,
if svc.ColorTemperature.MirekSchema.MirekMinimum != 0 {
hwState.ColorKelvinRange = &[2]int{
1000000 / svc.ColorTemperature.MirekSchema.MirekMinimum,
1000000 / svc.ColorTemperature.MirekSchema.MirekMaximum,
}
}
}
if svc.Color != nil {
+13
View File
@@ -7,6 +7,7 @@ import (
"git.aiterp.net/lucifer3/server/commands"
"git.aiterp.net/lucifer3/server/events"
"git.aiterp.net/lucifer3/server/internal/gentools"
"log"
"sync"
"time"
)
@@ -63,6 +64,18 @@ func (s *service) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Command
}
}
case commands.SearchDevices:
if sub, ok := command.Matches("hue"); ok {
if s.bridges[sub] != nil {
go func() {
err := s.bridges[sub].SearchDevices(time.Second * 30)
if err != nil {
log.Println("Search failed:", err)
}
}()
}
}
case commands.ConnectDevice:
if sub, ok := command.Matches("hue"); ok {
if s.bridges[sub] != nil {