Nik Afiq b54a747467
All checks were successful
CI / test (push) Successful in 5s
CI / build-ai-gateway (push) Successful in 53s
CI / build-ha-gateway (push) Successful in 42s
CI / build-discord-bot (push) Successful in 40s
feat: add switch control functionality to the Home Assistant integration
- Implemented TurnOn, TurnOff, and Toggle methods in the gRPC client for switch control.
- Added corresponding methods in the CommandApp to handle user commands for turning switches on, off, and toggling.
- Created unit tests for the new switch control methods in CommandApp and SwitchApp.
- Updated the HAGateway interface to include switch control methods.
- Enhanced the SwitchGRPC service to handle switch control requests and return appropriate responses.
- Added integration tests for the switch service to ensure correct behavior and error handling.
2026-07-22 23:44:35 +09:00

113 lines
3.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 SwitchApp struct {
ha driven.HAClient
mu sync.RWMutex
cache []domain.Switch
}
// NewSwitchApp constructs the switch application service.
func NewSwitchApp(ha driven.HAClient) *SwitchApp {
return &SwitchApp{ha: ha}
}
// Refresh repopulates the switch cache from the full Home Assistant state list.
func (a *SwitchApp) Refresh(ctx context.Context) error {
all, err := a.ha.ListStates(ctx)
if err != nil {
return err
}
var switches []domain.Switch
for _, s := range all {
if !strings.HasPrefix(s.EntityID, "switch.") {
continue
}
switches = append(switches, haStateToSwitch(s))
}
a.mu.Lock()
a.cache = switches
a.mu.Unlock()
return nil
}
// ListSwitches returns cached switch discovery data, refreshing lazily on first use.
func (a *SwitchApp) ListSwitches(ctx context.Context) ([]domain.Switch, 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 switch.turn_on call.
func (a *SwitchApp) TurnOn(ctx context.Context, id domain.EntityID) (*domain.EntityState, error) {
payload := map[string]any{"entity_id": string(id)}
return a.callService(ctx, "switch", "turn_on", payload)
}
// TurnOff maps application parameters into a Home Assistant switch.turn_off call.
func (a *SwitchApp) TurnOff(ctx context.Context, id domain.EntityID) (*domain.EntityState, error) {
payload := map[string]any{"entity_id": string(id)}
return a.callService(ctx, "switch", "turn_off", payload)
}
// Toggle maps directly to Home Assistant switch.toggle.
func (a *SwitchApp) Toggle(ctx context.Context, id domain.EntityID) (*domain.EntityState, error) {
payload := map[string]any{"entity_id": string(id)}
return a.callService(ctx, "switch", "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 *SwitchApp) 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
}
// haStateToSwitch extracts the subset of attributes needed for switch discovery.
func haStateToSwitch(s *driven.HAState) domain.Switch {
sw := domain.Switch{
EntityID: domain.EntityID(s.EntityID),
State: s.State,
}
if v, ok := s.Attributes["friendly_name"].(string); ok {
sw.FriendlyName = v
}
if v, ok := s.Attributes["device_class"].(string); ok {
sw.DeviceClass = v
}
return sw
}