- 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.
150 lines
4.1 KiB
Go
150 lines
4.1 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"sync"
|
|
|
|
"gitea.nik4nao.com/nik/home-services/ha-gateway/internal/core/domain"
|
|
"gitea.nik4nao.com/nik/home-services/ha-gateway/internal/core/ports/driven"
|
|
)
|
|
|
|
type LightApp struct {
|
|
ha driven.HAClient
|
|
mu sync.RWMutex
|
|
cache []domain.Light
|
|
}
|
|
|
|
// NewLightApp constructs the light application service.
|
|
func NewLightApp(ha driven.HAClient) *LightApp {
|
|
return &LightApp{ha: ha}
|
|
}
|
|
|
|
// Refresh repopulates the light cache from the full Home Assistant state list.
|
|
func (a *LightApp) Refresh(ctx context.Context) error {
|
|
all, err := a.ha.ListStates(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var lights []domain.Light
|
|
for _, s := range all {
|
|
if !strings.HasPrefix(s.EntityID, "light.") {
|
|
continue
|
|
}
|
|
lights = append(lights, haStateToLight(s))
|
|
}
|
|
|
|
a.mu.Lock()
|
|
a.cache = lights
|
|
a.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// ListLights returns cached light discovery data, refreshing lazily on first use.
|
|
func (a *LightApp) ListLights(ctx context.Context) ([]domain.Light, error) {
|
|
a.mu.RLock()
|
|
c := a.cache
|
|
a.mu.RUnlock()
|
|
if c == nil {
|
|
if err := a.Refresh(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
a.mu.RLock()
|
|
c = a.cache
|
|
a.mu.RUnlock()
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// TurnOn maps application parameters into a Home Assistant light.turn_on call.
|
|
func (a *LightApp) TurnOn(ctx context.Context, p domain.TurnOnParams) (*domain.EntityState, error) {
|
|
payload := map[string]any{"entity_id": string(p.EntityID)}
|
|
if p.BrightnessPct != nil {
|
|
payload["brightness_pct"] = *p.BrightnessPct
|
|
}
|
|
if p.ColorTempKelvin != nil {
|
|
payload["color_temp_kelvin"] = *p.ColorTempKelvin
|
|
}
|
|
if p.RGBColor != nil {
|
|
payload["rgb_color"] = []uint8{p.RGBColor.R, p.RGBColor.G, p.RGBColor.B}
|
|
}
|
|
if p.Transition != nil {
|
|
payload["transition"] = *p.Transition
|
|
}
|
|
return a.callService(ctx, "light", "turn_on", payload)
|
|
}
|
|
|
|
// TurnOff maps application parameters into a Home Assistant light.turn_off call.
|
|
func (a *LightApp) TurnOff(ctx context.Context, p domain.TurnOffParams) (*domain.EntityState, error) {
|
|
payload := map[string]any{"entity_id": string(p.EntityID)}
|
|
if p.Transition != nil {
|
|
payload["transition"] = *p.Transition
|
|
}
|
|
return a.callService(ctx, "light", "turn_off", payload)
|
|
}
|
|
|
|
// Toggle maps directly to Home Assistant light.toggle.
|
|
func (a *LightApp) Toggle(ctx context.Context, id domain.EntityID) (*domain.EntityState, error) {
|
|
payload := map[string]any{"entity_id": string(id)}
|
|
return a.callService(ctx, "light", "toggle", payload)
|
|
}
|
|
|
|
// callService falls back to GetState because Home Assistant may succeed without
|
|
// returning a full entity state list for the service call response.
|
|
func (a *LightApp) callService(ctx context.Context, svcDomain, service string, payload map[string]any) (*domain.EntityState, error) {
|
|
states, err := a.ha.CallService(ctx, svcDomain, service, payload)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
entityID, _ := payload["entity_id"].(string)
|
|
for _, s := range states {
|
|
if s.EntityID == entityID {
|
|
return haStateToDomain(s), nil
|
|
}
|
|
}
|
|
// HA may return an empty list on success; fall back to GetState.
|
|
s, err := a.ha.GetState(ctx, entityID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return haStateToDomain(s), nil
|
|
}
|
|
|
|
// haStateToLight extracts the subset of attributes needed for discovery clients.
|
|
func haStateToLight(s *driven.HAState) domain.Light {
|
|
l := domain.Light{
|
|
EntityID: domain.EntityID(s.EntityID),
|
|
State: s.State,
|
|
}
|
|
|
|
if v, ok := s.Attributes["friendly_name"].(string); ok {
|
|
l.FriendlyName = v
|
|
}
|
|
if v, ok := s.Attributes["is_hue_group"].(bool); ok {
|
|
l.IsHueGroup = v
|
|
}
|
|
if v, ok := s.Attributes["min_color_temp_kelvin"].(float64); ok {
|
|
l.MinColorTempKelvin = uint32(v)
|
|
}
|
|
if v, ok := s.Attributes["max_color_temp_kelvin"].(float64); ok {
|
|
l.MaxColorTempKelvin = uint32(v)
|
|
}
|
|
if modes, ok := s.Attributes["supported_color_modes"].([]any); ok {
|
|
for _, m := range modes {
|
|
if ms, ok := m.(string); ok {
|
|
l.SupportedColorModes = append(l.SupportedColorModes, domain.ColorMode(ms))
|
|
}
|
|
}
|
|
}
|
|
if effects, ok := s.Attributes["effect_list"].([]any); ok {
|
|
for _, e := range effects {
|
|
if es, ok := e.(string); ok {
|
|
l.EffectList = append(l.EffectList, es)
|
|
}
|
|
}
|
|
}
|
|
|
|
return l
|
|
}
|