|
|
@ -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 { |
|
|
|