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 }