Nik Afiq abb6774b77 feat: implement SwitchService with ListSwitches method
- Added ListSwitches method to SwitchService in switch_grpc.pb.go.
- Implemented SwitchGRPC adapter for ListSwitches in switch.go.
- Created SwitchApp for managing switch states and added ListSwitches logic.
- Updated core domain with Switch struct and associated methods.
- Enhanced LightApp to include ListLights functionality.
- Updated protobuf definitions for Switch and Light services to include new request and response messages.
- Introduced error handling for unimplemented methods in the gRPC server.
2026-04-06 19:25:06 +09:00

141 lines
3.5 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
}
func NewLightApp(ha driven.HAClient) *LightApp {
return &LightApp{ha: ha}
}
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
}
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
}
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)
}
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)
}
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)
}
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
}
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
}