All checks were successful
CI / changes (push) Successful in 19s
CI / test (push) Successful in 24s
CI / build-ai-gateway (push) Successful in 1m6s
CI / build-ha-gateway (push) Successful in 1m3s
CI / build-discord-bot (push) Successful in 1m4s
CI / build-alexa-bridge (push) Successful in 1m15s
CI / build-tts-gateway (push) Successful in 1m5s
CI / build-tts-sidecar (push) Has been skipped
CI / build-tts-model (push) Has been skipped
- Implemented SetTemperature in ClimateService for setting an absolute target temperature. - Updated ClimateServiceClient and ClimateServiceServer interfaces to include SetTemperature. - Added corresponding handler and tests for SetTemperature in ClimateGRPC. - Modified ClimateApp to handle SetTemperature requests without clamping. - Updated climate.proto to define SetTemperatureRequest message. - Adjusted Dockerfiles to include alexa-bridge dependencies.
85 lines
2.6 KiB
Go
85 lines
2.6 KiB
Go
package entities
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"gitea.nik4nao.com/nik/home-services/alexa-bridge/internal/core/domain"
|
|
hav1 "gitea.nik4nao.com/nik/home-services/gen/ha/v1"
|
|
)
|
|
|
|
// Client fetches resolvable entities from ha-gateway's EntityService. It
|
|
// calls ListStates once per configured domain (rather than one unfiltered
|
|
// call) since ListStatesRequest.domain is a single string, not repeated —
|
|
// see alexa-bridge/plan.md's Entity resolution section for why
|
|
// EntityService was chosen over the domain-specific ListLights/
|
|
// ListSwitches/ListClimates RPCs.
|
|
type Client struct {
|
|
entityClient hav1.EntityServiceClient
|
|
domains []string
|
|
}
|
|
|
|
// NewClient wraps an already-dialed EntityServiceClient — the connection
|
|
// itself is owned by internal/haclient.Client, shared across both packages
|
|
// rather than dialed twice. domains is the set of ha-gateway entity domains
|
|
// to resolve (e.g. "light", "switch", "climate"); RemoteService/SwitchBot
|
|
// entities are deliberately excluded (see plan.md Decisions #3).
|
|
func NewClient(entityClient hav1.EntityServiceClient, domains []string) *Client {
|
|
return &Client{entityClient: entityClient, domains: domains}
|
|
}
|
|
|
|
type fetchResult struct {
|
|
entities []domain.Entity
|
|
err error
|
|
}
|
|
|
|
// FetchAll fetches every entity across all configured domains, concurrently,
|
|
// and fails all-or-nothing: a partial entity list (e.g. missing every switch
|
|
// because just that one domain call failed) would silently break control
|
|
// for that domain without an obvious error, which is worse than a clean
|
|
// failure the caller can act on.
|
|
func (c *Client) FetchAll(ctx context.Context) ([]domain.Entity, error) {
|
|
results := make(chan fetchResult, len(c.domains))
|
|
for _, d := range c.domains {
|
|
go func(d string) {
|
|
resp, err := c.entityClient.ListStates(ctx, &hav1.ListStatesRequest{Domain: d})
|
|
if err != nil {
|
|
results <- fetchResult{err: fmt.Errorf("list states for domain %q: %w", d, err)}
|
|
return
|
|
}
|
|
out := make([]domain.Entity, 0, len(resp.GetStates()))
|
|
for _, s := range resp.GetStates() {
|
|
out = append(out, domain.Entity{
|
|
EntityID: s.GetEntityId(),
|
|
FriendlyName: firstNonEmpty(s.GetAttributes()["friendly_name"], s.GetEntityId()),
|
|
Domain: d,
|
|
})
|
|
}
|
|
results <- fetchResult{entities: out}
|
|
}(d)
|
|
}
|
|
|
|
var all []domain.Entity
|
|
var errs error
|
|
for range c.domains {
|
|
r := <-results
|
|
if r.err != nil {
|
|
errs = errors.Join(errs, r.err)
|
|
continue
|
|
}
|
|
all = append(all, r.entities...)
|
|
}
|
|
if errs != nil {
|
|
return nil, errs
|
|
}
|
|
return all, nil
|
|
}
|
|
|
|
func firstNonEmpty(a, b string) string {
|
|
if a != "" {
|
|
return a
|
|
}
|
|
return b
|
|
}
|