From edbc576eed71c3fbd6d5e6420bdec07b31c80c7b Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Thu, 29 Dec 2022 16:31:30 +0100 Subject: [PATCH] hue driver can set stuff now, I hope --- bus.go | 2 +- commands/state.go | 2 +- device/state.go | 7 ++ go.mod | 3 + go.sum | 2 + internal/color/color.go | 149 ++++++++++++++++++++++++ services/hue/bridge.go | 250 +++++++++++++++++++++++++++++++++++++--- services/hue/data.go | 15 ++- services/hue/service.go | 13 +++ 9 files changed, 420 insertions(+), 23 deletions(-) diff --git a/bus.go b/bus.go index 64fc2ba..7a8e050 100644 --- a/bus.go +++ b/bus.go @@ -65,7 +65,7 @@ func (b *EventBus) JoinPrivileged(service ActiveService) { } func (b *EventBus) RunCommand(command Command) { - if cd := command.CommandDescription(); !strings.HasPrefix(cd, "SetState") { + if cd := command.CommandDescription(); !strings.HasPrefix(cd, "SetStates(") { if setStates := atomic.LoadInt32(&b.setStates); setStates > 0 { fmt.Println("[INFO]", setStates, "SetStates commands hidden.") atomic.AddInt32(&b.setStates, -setStates) diff --git a/commands/state.go b/commands/state.go index e034ba3..bc5e0f8 100644 --- a/commands/state.go +++ b/commands/state.go @@ -20,7 +20,7 @@ func (c SetState) Matches(driver string) (sub string, ok bool) { } func (c SetState) CommandDescription() string { - return fmt.Sprintf("SetStates(%s, %s)", c.ID, c.State) + return fmt.Sprintf("SetState(%s, %s)", c.ID, c.State) } type SetStateBatch map[string]device.State diff --git a/device/state.go b/device/state.go index 4f6b029..8d46ba1 100644 --- a/device/state.go +++ b/device/state.go @@ -14,6 +14,13 @@ type State struct { Color *color.Color `json:"color"` } +func (s State) Empty() bool { + return s.Power == nil && + s.Temperature == nil && + s.Intensity == nil && + s.Color == nil +} + func (s State) String() string { parts := make([]string, 0, 4) if s.Power != nil { diff --git a/go.mod b/go.mod index a613064..ca45253 100644 --- a/go.mod +++ b/go.mod @@ -3,4 +3,7 @@ module git.aiterp.net/lucifer3/server go 1.19 require github.com/lucasb-eyer/go-colorful v1.2.0 + require github.com/gobwas/glob v0.2.3 + +require golang.org/x/sync v0.1.0 // indirect diff --git a/go.sum b/go.sum index ffef3ce..fbf32fd 100644 --- a/go.sum +++ b/go.sum @@ -2,3 +2,5 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/internal/color/color.go b/internal/color/color.go index 5a7f972..11176fd 100644 --- a/internal/color/color.go +++ b/internal/color/color.go @@ -104,6 +104,10 @@ func (col *Color) ToRGB() (col2 Color, ok bool) { rgb := col.XY.ToRGB() col2 = Color{RGB: &rgb} ok = true + } else if col.K != nil { + rgb := kToRGB(*col.K) + col2 = Color{RGB: &rgb} + ok = true } return @@ -122,6 +126,10 @@ func (col *Color) ToHS() (col2 Color, ok bool) { hs := col.XY.ToHS() col2 = Color{HS: &hs} ok = true + } else if col.K != nil { + hs := kToRGB(*col.K).ToHS() + col2 = Color{HS: &hs} + ok = true } return @@ -174,6 +182,10 @@ func (col *Color) ToXY() (col2 Color, ok bool) { xy := col.RGB.ToXY() col2 = Color{XY: &xy} ok = true + } else if col.K != nil { + xy := kToRGB(*col.K).ToXY() + col2 = Color{XY: &xy} + ok = true } return @@ -436,3 +448,140 @@ func hex2digit(h byte) int { return int(h - '0') } } + +func kToRGB(kelvin int) RGB { + if kelvin < 1000 { + kelvin = 1000 + } else if kelvin > 12000 { + kelvin = 12000 + } + if kelvin%100 == 0 { + return kelvinRGBTable[kelvin] + } + + fac := float64(kelvin%100) / 100 + floor := (kelvin / 100) * 100 + ceil := floor + 100 + floorRGB := kelvinRGBTable[floor] + ceilRGB := kelvinRGBTable[ceil] + + return RGB{ + Red: (floorRGB.Red * fac) + (ceilRGB.Red * (1 - fac)), + Green: (floorRGB.Green * fac) + (ceilRGB.Green * (1 - fac)), + Blue: (floorRGB.Blue * fac) + (ceilRGB.Blue * (1 - fac)), + } +} + +var kelvinRGBTable = map[int]RGB{ + 1000: {Red: 1.000, Green: 0.220, Blue: 0.000}, + 1100: {Red: 1.000, Green: 0.278, Blue: 0.000}, + 1200: {Red: 1.000, Green: 0.325, Blue: 0.000}, + 1300: {Red: 1.000, Green: 0.365, Blue: 0.000}, + 1400: {Red: 1.000, Green: 0.396, Blue: 0.000}, + 1500: {Red: 1.000, Green: 0.427, Blue: 0.000}, + 1600: {Red: 1.000, Green: 0.451, Blue: 0.000}, + 1700: {Red: 1.000, Green: 0.475, Blue: 0.000}, + 1800: {Red: 1.000, Green: 0.494, Blue: 0.000}, + 1900: {Red: 1.000, Green: 0.514, Blue: 0.000}, + 2000: {Red: 1.000, Green: 0.541, Blue: 0.071}, + 2100: {Red: 1.000, Green: 0.557, Blue: 0.129}, + 2200: {Red: 1.000, Green: 0.576, Blue: 0.173}, + 2300: {Red: 1.000, Green: 0.596, Blue: 0.212}, + 2400: {Red: 1.000, Green: 0.616, Blue: 0.247}, + 2500: {Red: 1.000, Green: 0.631, Blue: 0.282}, + 2600: {Red: 1.000, Green: 0.647, Blue: 0.310}, + 2700: {Red: 1.000, Green: 0.663, Blue: 0.341}, + 2800: {Red: 1.000, Green: 0.678, Blue: 0.369}, + 2900: {Red: 1.000, Green: 0.694, Blue: 0.396}, + 3000: {Red: 1.000, Green: 0.706, Blue: 0.420}, + 3100: {Red: 1.000, Green: 0.722, Blue: 0.447}, + 3200: {Red: 1.000, Green: 0.733, Blue: 0.471}, + 3300: {Red: 1.000, Green: 0.745, Blue: 0.494}, + 3400: {Red: 1.000, Green: 0.757, Blue: 0.518}, + 3500: {Red: 1.000, Green: 0.769, Blue: 0.537}, + 3600: {Red: 1.000, Green: 0.780, Blue: 0.561}, + 3700: {Red: 1.000, Green: 0.788, Blue: 0.580}, + 3800: {Red: 1.000, Green: 0.800, Blue: 0.600}, + 3900: {Red: 1.000, Green: 0.808, Blue: 0.624}, + 4000: {Red: 1.000, Green: 0.820, Blue: 0.639}, + 4100: {Red: 1.000, Green: 0.827, Blue: 0.659}, + 4200: {Red: 1.000, Green: 0.835, Blue: 0.678}, + 4300: {Red: 1.000, Green: 0.843, Blue: 0.694}, + 4400: {Red: 1.000, Green: 0.851, Blue: 0.714}, + 4500: {Red: 1.000, Green: 0.859, Blue: 0.729}, + 4600: {Red: 1.000, Green: 0.867, Blue: 0.745}, + 4700: {Red: 1.000, Green: 0.875, Blue: 0.761}, + 4800: {Red: 1.000, Green: 0.882, Blue: 0.776}, + 4900: {Red: 1.000, Green: 0.890, Blue: 0.792}, + 5000: {Red: 1.000, Green: 0.894, Blue: 0.808}, + 5100: {Red: 1.000, Green: 0.902, Blue: 0.824}, + 5200: {Red: 1.000, Green: 0.910, Blue: 0.835}, + 5300: {Red: 1.000, Green: 0.914, Blue: 0.851}, + 5400: {Red: 1.000, Green: 0.922, Blue: 0.863}, + 5500: {Red: 1.000, Green: 0.925, Blue: 0.878}, + 5600: {Red: 1.000, Green: 0.933, Blue: 0.890}, + 5700: {Red: 1.000, Green: 0.937, Blue: 0.902}, + 5800: {Red: 1.000, Green: 0.941, Blue: 0.914}, + 5900: {Red: 1.000, Green: 0.949, Blue: 0.925}, + 6000: {Red: 1.000, Green: 0.953, Blue: 0.937}, + 6100: {Red: 1.000, Green: 0.957, Blue: 0.949}, + 6200: {Red: 1.000, Green: 0.961, Blue: 0.961}, + 6300: {Red: 1.000, Green: 0.965, Blue: 0.969}, + 6400: {Red: 1.000, Green: 0.973, Blue: 0.984}, + 6500: {Red: 1.000, Green: 0.976, Blue: 0.992}, + 6600: {Red: 0.996, Green: 0.976, Blue: 1.000}, + 6700: {Red: 0.988, Green: 0.969, Blue: 1.000}, + 6800: {Red: 0.976, Green: 0.965, Blue: 1.000}, + 6900: {Red: 0.969, Green: 0.961, Blue: 1.000}, + 7000: {Red: 0.961, Green: 0.953, Blue: 1.000}, + 7100: {Red: 0.953, Green: 0.949, Blue: 1.000}, + 7200: {Red: 0.941, Green: 0.945, Blue: 1.000}, + 7300: {Red: 0.937, Green: 0.941, Blue: 1.000}, + 7400: {Red: 0.929, Green: 0.937, Blue: 1.000}, + 7500: {Red: 0.922, Green: 0.933, Blue: 1.000}, + 7600: {Red: 0.914, Green: 0.929, Blue: 1.000}, + 7700: {Red: 0.906, Green: 0.925, Blue: 1.000}, + 7800: {Red: 0.902, Green: 0.922, Blue: 1.000}, + 7900: {Red: 0.894, Green: 0.918, Blue: 1.000}, + 8000: {Red: 0.890, Green: 0.914, Blue: 1.000}, + 8100: {Red: 0.882, Green: 0.910, Blue: 1.000}, + 8200: {Red: 0.878, Green: 0.906, Blue: 1.000}, + 8300: {Red: 0.871, Green: 0.902, Blue: 1.000}, + 8400: {Red: 0.867, Green: 0.902, Blue: 1.000}, + 8500: {Red: 0.863, Green: 0.898, Blue: 1.000}, + 8600: {Red: 0.855, Green: 0.898, Blue: 1.000}, + 8700: {Red: 0.851, Green: 0.890, Blue: 1.000}, + 8800: {Red: 0.847, Green: 0.890, Blue: 1.000}, + 8900: {Red: 0.843, Green: 0.886, Blue: 1.000}, + 9000: {Red: 0.839, Green: 0.882, Blue: 1.000}, + 9100: {Red: 0.831, Green: 0.882, Blue: 1.000}, + 9200: {Red: 0.827, Green: 0.878, Blue: 1.000}, + 9300: {Red: 0.824, Green: 0.875, Blue: 1.000}, + 9400: {Red: 0.820, Green: 0.875, Blue: 1.000}, + 9500: {Red: 0.816, Green: 0.871, Blue: 1.000}, + 9600: {Red: 0.812, Green: 0.867, Blue: 1.000}, + 9700: {Red: 0.812, Green: 0.867, Blue: 1.000}, + 9800: {Red: 0.808, Green: 0.863, Blue: 1.000}, + 9900: {Red: 0.804, Green: 0.863, Blue: 1.000}, + 10000: {Red: 0.812, Green: 0.855, Blue: 1.000}, + 10100: {Red: 0.812, Green: 0.855, Blue: 1.000}, + 10200: {Red: 0.808, Green: 0.851, Blue: 1.000}, + 10300: {Red: 0.804, Green: 0.851, Blue: 1.000}, + 10400: {Red: 0.800, Green: 0.847, Blue: 1.000}, + 10500: {Red: 0.800, Green: 0.847, Blue: 1.000}, + 10600: {Red: 0.796, Green: 0.843, Blue: 1.000}, + 10700: {Red: 0.792, Green: 0.843, Blue: 1.000}, + 10800: {Red: 0.792, Green: 0.839, Blue: 1.000}, + 10900: {Red: 0.788, Green: 0.839, Blue: 1.000}, + 11000: {Red: 0.784, Green: 0.835, Blue: 1.000}, + 11100: {Red: 0.784, Green: 0.835, Blue: 1.000}, + 11200: {Red: 0.780, Green: 0.831, Blue: 1.000}, + 11300: {Red: 0.776, Green: 0.831, Blue: 1.000}, + 11400: {Red: 0.776, Green: 0.831, Blue: 1.000}, + 11500: {Red: 0.773, Green: 0.827, Blue: 1.000}, + 11600: {Red: 0.773, Green: 0.827, Blue: 1.000}, + 11700: {Red: 0.773, Green: 0.824, Blue: 1.000}, + 11800: {Red: 0.769, Green: 0.824, Blue: 1.000}, + 11900: {Red: 0.765, Green: 0.824, Blue: 1.000}, + 12000: {Red: 0.765, Green: 0.820, Blue: 1.000}, +} diff --git a/services/hue/bridge.go b/services/hue/bridge.go index dd97b93..3df3c80 100644 --- a/services/hue/bridge.go +++ b/services/hue/bridge.go @@ -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: + } } diff --git a/services/hue/data.go b/services/hue/data.go index 4d98dae..280cff8 100644 --- a/services/hue/data.go +++ b/services/hue/data.go @@ -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 { diff --git a/services/hue/service.go b/services/hue/service.go index e55698a..cdcef92 100644 --- a/services/hue/service.go +++ b/services/hue/service.go @@ -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 {