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 }