From b191455753daeabcfc9e7868b62958ad5792fcf8 Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Sat, 30 Oct 2021 12:05:56 +0200 Subject: [PATCH] optimize Hue light updates by using temporary groups. --- app/api/devices.go | 10 +-- internal/drivers/hue/bridge.go | 146 +++++++++++++++++++++++++++++---- internal/drivers/hue/data.go | 88 ++++++++++++++++++++ internal/drivers/hue/state.go | 35 +++++++- 4 files changed, 258 insertions(+), 21 deletions(-) diff --git a/app/api/devices.go b/app/api/devices.go index c524b05..038dee0 100644 --- a/app/api/devices.go +++ b/app/api/devices.go @@ -137,12 +137,10 @@ func Devices(r gin.IRoutes) { config.PublishChannel <- devices - go func() { - err = config.DeviceRepository().SaveMany(ctxOf(c), models.SMState, devices) - if err != nil { - log.Println("Failed to save devices states") - } - }() + err = config.DeviceRepository().SaveMany(ctxOf(c), models.SMState, devices) + if err != nil { + log.Println("Failed to save devices states") + } return withSceneState(devices), nil })) diff --git a/internal/drivers/hue/bridge.go b/internal/drivers/hue/bridge.go index b8b64a2..ca6686e 100644 --- a/internal/drivers/hue/bridge.go +++ b/internal/drivers/hue/bridge.go @@ -6,10 +6,11 @@ import ( "encoding/json" "fmt" "git.aiterp.net/lucifer/new-server/models" - "golang.org/x/sync/errgroup" "io" + "log" "net" "net/http" + "strconv" "strings" "sync" "time" @@ -97,17 +98,17 @@ func (b *Bridge) Refresh(ctx context.Context) error { func (b *Bridge) SyncStale(ctx context.Context) error { indices := make([]int, 0, 4) + arrayIndices := make([]int, 0, 4) inputs := make([]LightStateInput, 0, 4) - eg, ctx := errgroup.WithContext(ctx) - b.mu.Lock() - for _, state := range b.lightStates { + for i, state := range b.lightStates { if !state.stale { continue } indices = append(indices, state.index) + arrayIndices = append(arrayIndices, i) inputs = append(inputs, state.input) } b.mu.Unlock() @@ -116,24 +117,85 @@ func (b *Bridge) SyncStale(ctx context.Context) error { return nil } - for i, input := range inputs { - iCopy := i - index := indices[i] - inputCopy := input + groups := make([]*syncGroup, 0, 4) + for i := range inputs { + input := inputs[i] + + found := false + for _, group := range groups { + if group.State.Equal(input) { + group.Indexes = append(group.Indexes, indices[i]) + group.ArrayIndexes = append(group.ArrayIndexes, arrayIndices[i]) + found = true + } + } + if !found { + groups = append(groups, &syncGroup{ + GroupIndex: -1, + State: input, + Indexes: []int{indices[i]}, + ArrayIndexes: []int{arrayIndices[i]}, + }) + } + } + + groupMap, err := b.getGroups(ctx) + if err != nil { + return err + } + + for id, data := range groupMap { + for _, group := range groups { + if group.Matches(&data) { + group.GroupIndex = id + break + } + } + } + + for _, group := range groups { + if group.GroupIndex == -1 { + data := GroupData{ + Name: "lucifer_auto_group", + Lights: []string{}, + } - eg.Go(func() error { - err := b.putLightState(ctx, index, inputCopy) + for _, idx := range group.Indexes { + data.Lights = append(data.Lights, strconv.Itoa(idx)) + } + + id, err := b.postGroup(ctx, data) if err != nil { return err } - b.lightStates[iCopy].stale = false + group.GroupIndex = id + } + } + + log.Println("Updating", len(inputs), "lights on Hue bridge", b.externalID, "in", len(groups), "groups") + + for _, group := range groups { + err := b.putGroupLightState(ctx, group.GroupIndex, group.State) + if err != nil { + return err + } + + b.mu.Lock() + for _, arrayIndex := range group.ArrayIndexes { + b.lightStates[arrayIndex].stale = false + } + b.mu.Unlock() - return nil - }) + if groupMap[group.GroupIndex].Name == "lucifer_auto_group" { + err := b.deleteGroup(ctx, group.GroupIndex) + if err != nil { + log.Println("Could not delete temporary group", err) + } + } } - return eg.Wait() + return nil } func (b *Bridge) SyncSensors(ctx context.Context) ([]models.Event, error) { @@ -170,6 +232,10 @@ func (b *Bridge) putLightState(ctx context.Context, index int, input LightStateI return b.put(ctx, fmt.Sprintf("lights/%d/state", index), input, nil) } +func (b *Bridge) putGroupLightState(ctx context.Context, index int, input LightStateInput) error { + return b.put(ctx, fmt.Sprintf("groups/%d/action", index), input, nil) +} + func (b *Bridge) getToken(ctx context.Context) (string, error) { result := make([]CreateUserResponse, 0, 1) err := b.post(ctx, "", CreateUserInput{DeviceType: "git.aiterp.net/lucifer"}, &result) @@ -207,6 +273,34 @@ func (b *Bridge) getSensors(ctx context.Context) (map[int]SensorData, error) { return result, nil } +func (b *Bridge) getGroups(ctx context.Context) (map[int]GroupData, error) { + result := make(map[int]GroupData, 16) + err := b.get(ctx, "groups", &result) + if err != nil { + return nil, err + } + + return result, nil +} + +func (b *Bridge) postGroup(ctx context.Context, input GroupData) (int, error) { + var res []struct { + Success struct { + ID string `json:"id"` + } `json:"success"` + } + err := b.post(ctx, "groups", input, &res) + + id, _ := strconv.Atoi(res[0].Success.ID) + + return id, err +} + + +func (b *Bridge) deleteGroup(ctx context.Context, index int) error { + return b.delete(ctx, "groups/"+strconv.Itoa(index), nil) +} + func (b *Bridge) get(ctx context.Context, resource string, target interface{}) error { if b.token != "" { resource = b.token + "/" + resource @@ -226,6 +320,30 @@ func (b *Bridge) get(ctx context.Context, resource string, target interface{}) e return json.NewDecoder(res.Body).Decode(target) } +func (b *Bridge) delete(ctx context.Context, resource string, target interface{}) error { + if b.token != "" { + resource = b.token + "/" + resource + } + + req, err := http.NewRequest("DELETE", fmt.Sprintf("http://%s/api/%s", b.host, resource), nil) + if err != nil { + return err + } + + res, err := httpClient.Do(req.WithContext(ctx)) + if err != nil { + return err + } + defer res.Body.Close() + + if target == nil { + return nil + } + + return json.NewDecoder(res.Body).Decode(target) +} + + func (b *Bridge) post(ctx context.Context, resource string, body interface{}, target interface{}) error { rb, err := reqBody(body) if err != nil { diff --git a/internal/drivers/hue/data.go b/internal/drivers/hue/data.go index 962f4d1..f5d78f6 100644 --- a/internal/drivers/hue/data.go +++ b/internal/drivers/hue/data.go @@ -3,8 +3,14 @@ package hue import ( "encoding/xml" "errors" + "strconv" ) +type GroupData struct { + Name string `json:"name"` + Lights []string `json:"lights"` +} + type DiscoveryEntry struct { Id string `json:"id"` InternalIPAddress string `json:"internalipaddress"` @@ -83,6 +89,58 @@ type LightStateInput struct { TransitionTime *int `json:"transitiontime,omitempty"` } +func (input *LightStateInput) Equal(other LightStateInput) bool { + if (input.On != nil) != (other.On != nil) { + return false + } else if input.On != nil && *input.On != *other.On { + return false + } + + if (input.Bri != nil) != (other.Bri != nil) { + return false + } else if input.Bri != nil && *input.Bri != *other.Bri { + return false + } + + if (input.Hue != nil) != (other.Hue != nil) { + return false + } else if input.Hue != nil && *input.Hue != *other.Hue { + return false + } + + if (input.Sat != nil) != (other.Sat != nil) { + return false + } else if input.Sat != nil && *input.Sat != *other.Sat { + return false + } + + if (input.Effect != nil) != (other.Effect != nil) { + return false + } else if input.Effect != nil && *input.Effect != *other.Effect { + return false + } + + if (input.XY != nil) != (other.XY != nil) { + return false + } else if input.XY != nil && *input.XY != *other.XY { + return false + } + + if (input.CT != nil) != (other.CT != nil) { + return false + } else if input.CT != nil && *input.CT != *other.CT { + return false + } + + if (input.Alert != nil) != (other.Alert != nil) { + return false + } else if input.Alert != nil && *input.Alert != *other.Alert { + return false + } + + return true +} + type LightData struct { State LightState `json:"state"` Type string `json:"type"` @@ -192,6 +250,36 @@ type BridgeDeviceInfo struct { } `xml:"device"` } +type syncGroup struct { + GroupIndex int + State LightStateInput + Indexes []int + ArrayIndexes []int +} + +func (sg *syncGroup) Matches(group *GroupData) bool { + if len(sg.Indexes) != len(group.Lights) { + return false + } + + for _, idx := range sg.Indexes { + found := false + for _, idx2 := range group.Lights { + n := strconv.Itoa(idx) + if n == idx2 { + found = true + break + } + } + + if !found { + return false + } + } + + return true +} + var buttonNames = []string{"On", "DimUp", "DimDown", "Off"} var errLinkButtonNotPressed = errors.New("link button not pressed") diff --git a/internal/drivers/hue/state.go b/internal/drivers/hue/state.go index 588f4ae..2e89698 100644 --- a/internal/drivers/hue/state.go +++ b/internal/drivers/hue/state.go @@ -1,6 +1,7 @@ package hue import ( + "encoding/json" "git.aiterp.net/lucifer/new-server/models" "strconv" "time" @@ -16,12 +17,37 @@ type hueLightState struct { stale bool } +func (s *hueLightState) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Index int `json:"index"` + UniqueID string `json:"uniqueId"` + ExternalID int `json:"externalId"` + Info LightData `json:"info"` + Input LightStateInput `json:"input"` + Stale bool `json:"stale"` + }{ + Index: s.index, + UniqueID: s.uniqueID, + ExternalID: s.externalID, + Info: s.info, + Input: s.input, + Stale: s.stale, + }) +} + func (s *hueLightState) Update(state models.DeviceState) { input := LightStateInput{} if state.Power { input.On = ptrBool(true) if state.Color.IsKelvin() { input.CT = ptrInt(1000000 / state.Color.Kelvin) + if *input.CT < s.info.Capabilities.Control.CT.Min { + *input.CT = s.info.Capabilities.Control.CT.Min + } + if *input.CT > s.info.Capabilities.Control.CT.Max { + *input.CT = s.info.Capabilities.Control.CT.Max + } + if s.input.CT == nil || *s.input.CT != *input.CT { s.stale = true } @@ -67,6 +93,13 @@ func (s *hueLightState) Update(state models.DeviceState) { } func (s *hueLightState) CheckStaleness(state LightState) { + if s.input.On == nil || state.On != *s.input.On { + s.stale = true + } + if !state.On { + return + } + if state.ColorMode == "xy" { s.stale = true if s.input.CT == nil && s.input.Hue == nil { @@ -80,7 +113,7 @@ func (s *hueLightState) CheckStaleness(state LightState) { s.stale = true } } else { - if s.input.Hue == nil || state.Hue != *s.input.Hue || s.input.Sat == nil || state.Sat == *s.input.Sat { + if s.input.Hue == nil || state.Hue != *s.input.Hue || s.input.Sat == nil || state.Sat != *s.input.Sat { s.stale = true } }