package haclient import ( "fmt" "strconv" hav1 "gitea.nik4nao.com/nik/home-services/gen/ha/v1" ) // optUint32 returns nil when key is absent. A present-but-unparsable value is // treated as absent rather than failing the whole action — brightness/ // color-temp/transition are optional TurnOn/TurnOff fields, so a bad value // there shouldn't block the rest of the request. func optUint32(params map[string]any, key string) *uint32 { v, ok := params[key] if !ok { return nil } n, err := toUint32(v) if err != nil { return nil } return &n } func reqUint32(params map[string]any, key string) (uint32, error) { v, ok := params[key] if !ok { return 0, fmt.Errorf("missing required param %q", key) } return toUint32(v) } func reqFloat64(params map[string]any, key string) (float64, error) { v, ok := params[key] if !ok { return 0, fmt.Errorf("missing required param %q", key) } return toFloat64(v) } func reqString(params map[string]any, key string) (string, error) { v, ok := params[key] if !ok { return "", fmt.Errorf("missing required param %q", key) } s, ok := v.(string) if !ok { return "", fmt.Errorf("param %q = %v (%T), want string", key, v, v) } return s, nil } // optRGB mirrors optUint32's absent-or-unparsable-means-nil contract. func optRGB(params map[string]any, key string) *hav1.RGBColor { rgb, err := parseRGB(params, key) if err != nil { return nil } return rgb } func reqRGB(params map[string]any, key string) (*hav1.RGBColor, error) { rgb, err := parseRGB(params, key) if err != nil { return nil, err } if rgb == nil { return nil, fmt.Errorf("missing required param %q", key) } return rgb, nil } func parseRGB(params map[string]any, key string) (*hav1.RGBColor, error) { v, ok := params[key] if !ok { return nil, nil } m, ok := v.(map[string]any) if !ok { return nil, fmt.Errorf("param %q = %v (%T), want map with r/g/b", key, v, v) } r, err := toUint32(m["r"]) if err != nil { return nil, fmt.Errorf("param %q.r: %w", key, err) } g, err := toUint32(m["g"]) if err != nil { return nil, fmt.Errorf("param %q.g: %w", key, err) } b, err := toUint32(m["b"]) if err != nil { return nil, fmt.Errorf("param %q.b: %w", key, err) } return &hav1.RGBColor{R: r, G: g, B: b}, nil } // toUint32/toFloat64 primarily parse strings, since params values arriving // from Alexa slots (via internal/directive) are always strings — even // AMAZON.NUMBER slots decode to their literal spoken-number string, e.g. // "80". The float64/int fallback exists only so tests (or a hypothetical // future non-Alexa caller) can pass typed values directly; it isn't // exercised on the real Alexa -> directive -> haclient path. func toUint32(v any) (uint32, error) { switch t := v.(type) { case string: n, err := strconv.ParseUint(t, 10, 32) if err != nil { return 0, fmt.Errorf("parse uint32 %q: %w", t, err) } return uint32(n), nil case float64: return uint32(t), nil case int: return uint32(t), nil case uint32: return t, nil default: return 0, fmt.Errorf("value %v (%T) is not a uint32-compatible type", v, v) } } func toFloat64(v any) (float64, error) { switch t := v.(type) { case string: n, err := strconv.ParseFloat(t, 64) if err != nil { return 0, fmt.Errorf("parse float64 %q: %w", t, err) } return n, nil case float64: return t, nil case int: return float64(t), nil default: return 0, fmt.Errorf("value %v (%T) is not a float64-compatible type", v, v) } }