Nik Afiq 8f7024edfa
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
feat(climate): add SetTemperature method to ClimateService
- 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.
2026-07-25 12:08:24 +09:00

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
}