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 }