- Added detailed comments to clarify the purpose of various functions and types in the Discord bot and HA gateway. - Introduced new methods in the CommandApp for handling light and switch operations, including HandleLightOn, HandleLightOff, HandleLightToggle, and their respective autocomplete functions. - Updated the HAClient interface to include methods for fetching states and calling services, enhancing the interaction with Home Assistant. - Improved the structure of entity and light domain models to include additional attributes and clearer documentation. - Implemented logging enhancements in both the Discord bot and HA gateway to ensure better traceability and context in logs. - Refactored the configuration loading process to streamline environment variable handling and defaults. - Stubbed out switch control methods in the gRPC adapter, indicating future implementation plans. - Enhanced telemetry setup to ensure proper initialization and shutdown procedures for observability.
167 lines
5.6 KiB
Go
167 lines
5.6 KiB
Go
package gateway
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"gitea.nik4nao.com/nik/home-services/discord-bot/internal/core/ports/driven"
|
|
"gitea.nik4nao.com/nik/home-services/discord-bot/internal/logger"
|
|
hav1 "gitea.nik4nao.com/nik/home-services/gen/ha/v1"
|
|
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
)
|
|
|
|
// Client implements the app's HA driven port over gRPC.
|
|
type Client struct {
|
|
conn *grpc.ClientConn
|
|
lightClient hav1.LightServiceClient
|
|
switchClient hav1.SwitchServiceClient
|
|
log *slog.Logger
|
|
}
|
|
|
|
// New constructs a gRPC client for the internal ha-gateway service.
|
|
func New(ctx context.Context, addr string, log *slog.Logger) (*Client, error) {
|
|
conn, err := grpc.NewClient(
|
|
addr,
|
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("dial ha-gateway: %w", err)
|
|
}
|
|
|
|
return &Client{
|
|
conn: conn,
|
|
lightClient: hav1.NewLightServiceClient(conn),
|
|
switchClient: hav1.NewSwitchServiceClient(conn),
|
|
log: log,
|
|
}, nil
|
|
}
|
|
|
|
// Close closes the underlying gRPC connection.
|
|
func (c *Client) Close() error {
|
|
if err := c.conn.Close(); err != nil {
|
|
return fmt.Errorf("close ha-gateway client: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ListLights calls ha-gateway discovery RPCs and maps protobuf messages into
|
|
// the driven port type expected by the app layer.
|
|
func (c *Client) ListLights(ctx context.Context) ([]driven.Light, error) {
|
|
start := time.Now()
|
|
log := logger.FromContext(ctx).With("grpc.method", "LightService/ListLights")
|
|
resp, err := c.lightClient.ListLights(ctx, &hav1.ListLightsRequest{})
|
|
if err != nil {
|
|
log.Error("grpc call failed",
|
|
"duration_ms", time.Since(start).Milliseconds(),
|
|
"error", err.Error(),
|
|
)
|
|
return nil, fmt.Errorf("list lights: %w", err)
|
|
}
|
|
log.Debug("grpc call completed", "duration_ms", time.Since(start).Milliseconds())
|
|
|
|
lights := make([]driven.Light, 0, len(resp.GetLights()))
|
|
for _, light := range resp.GetLights() {
|
|
lights = append(lights, driven.Light{
|
|
EntityID: light.GetEntityId(),
|
|
FriendlyName: light.GetFriendlyName(),
|
|
State: light.GetState(),
|
|
SupportedColorModes: append([]string(nil), light.GetSupportedColorModes()...),
|
|
MinColorTempKelvin: light.GetMinColorTempKelvin(),
|
|
MaxColorTempKelvin: light.GetMaxColorTempKelvin(),
|
|
IsHueGroup: light.GetIsHueGroup(),
|
|
EffectList: append([]string(nil), light.GetEffectList()...),
|
|
})
|
|
}
|
|
return lights, nil
|
|
}
|
|
|
|
// ListSwitches calls ha-gateway discovery RPCs and maps protobuf messages into
|
|
// the driven port type expected by the app layer.
|
|
func (c *Client) ListSwitches(ctx context.Context) ([]driven.Switch, error) {
|
|
start := time.Now()
|
|
log := logger.FromContext(ctx).With("grpc.method", "SwitchService/ListSwitches")
|
|
resp, err := c.switchClient.ListSwitches(ctx, &hav1.ListSwitchesRequest{})
|
|
if err != nil {
|
|
log.Error("grpc call failed",
|
|
"duration_ms", time.Since(start).Milliseconds(),
|
|
"error", err.Error(),
|
|
)
|
|
return nil, fmt.Errorf("list switches: %w", err)
|
|
}
|
|
log.Debug("grpc call completed", "duration_ms", time.Since(start).Milliseconds())
|
|
|
|
switches := make([]driven.Switch, 0, len(resp.GetSwitches()))
|
|
for _, sw := range resp.GetSwitches() {
|
|
switches = append(switches, driven.Switch{
|
|
EntityID: sw.GetEntityId(),
|
|
FriendlyName: sw.GetFriendlyName(),
|
|
State: sw.GetState(),
|
|
DeviceClass: sw.GetDeviceClass(),
|
|
})
|
|
}
|
|
return switches, nil
|
|
}
|
|
|
|
// TurnOnLight forwards a light turn-on request over gRPC.
|
|
func (c *Client) TurnOnLight(ctx context.Context, entityID string, brightnessPct *uint32, colorTempKelvin *uint32) error {
|
|
start := time.Now()
|
|
log := logger.FromContext(ctx).With("grpc.method", "LightService/TurnOn")
|
|
req := &hav1.TurnOnRequest{EntityId: entityID}
|
|
if brightnessPct != nil {
|
|
req.BrightnessPct = brightnessPct
|
|
}
|
|
if colorTempKelvin != nil {
|
|
req.ColorTempKelvin = colorTempKelvin
|
|
}
|
|
|
|
if _, err := c.lightClient.TurnOn(ctx, req); err != nil {
|
|
log.Error("grpc call failed",
|
|
"duration_ms", time.Since(start).Milliseconds(),
|
|
"error", err.Error(),
|
|
)
|
|
return fmt.Errorf("turn on light %s: %w", entityID, err)
|
|
}
|
|
log.Debug("grpc call completed", "duration_ms", time.Since(start).Milliseconds())
|
|
return nil
|
|
}
|
|
|
|
// TurnOffLight forwards a light turn-off request over gRPC.
|
|
func (c *Client) TurnOffLight(ctx context.Context, entityID string, transition *uint32) error {
|
|
start := time.Now()
|
|
log := logger.FromContext(ctx).With("grpc.method", "LightService/TurnOff")
|
|
req := &hav1.TurnOffRequest{EntityId: entityID}
|
|
if transition != nil {
|
|
req.Transition = transition
|
|
}
|
|
|
|
if _, err := c.lightClient.TurnOff(ctx, req); err != nil {
|
|
log.Error("grpc call failed",
|
|
"duration_ms", time.Since(start).Milliseconds(),
|
|
"error", err.Error(),
|
|
)
|
|
return fmt.Errorf("turn off light %s: %w", entityID, err)
|
|
}
|
|
log.Debug("grpc call completed", "duration_ms", time.Since(start).Milliseconds())
|
|
return nil
|
|
}
|
|
|
|
// ToggleLight forwards a light toggle request over gRPC.
|
|
func (c *Client) ToggleLight(ctx context.Context, entityID string) error {
|
|
start := time.Now()
|
|
log := logger.FromContext(ctx).With("grpc.method", "LightService/Toggle")
|
|
if _, err := c.lightClient.Toggle(ctx, &hav1.ToggleRequest{EntityId: entityID}); err != nil {
|
|
log.Error("grpc call failed",
|
|
"duration_ms", time.Since(start).Milliseconds(),
|
|
"error", err.Error(),
|
|
)
|
|
return fmt.Errorf("toggle light %s: %w", entityID, err)
|
|
}
|
|
log.Debug("grpc call completed", "duration_ms", time.Since(start).Milliseconds())
|
|
return nil
|
|
}
|