// Package collector maps Shelly product types to Prometheus collectors. package collector import ( "fmt" "sort" "strings" "sync" "unicode" "github.com/prometheus/client_golang/prometheus" "lostak.dev/shelly-exporter/configuration" "lostak.dev/shelly-exporter/shelly" ) // Factory constructs a collector for one configured device. type Factory func(configuration.DeviceConfiguration, shelly.StatusClient) (prometheus.Collector, error) var ( factoriesMutex sync.RWMutex factories = make(map[string]Factory) ) // RegisterProduct adds support for a product identifier. Product packages call it from init. func RegisterProduct(product string, factory Factory) error { product = normalizeProduct(product) if product == "" { return fmt.Errorf("product identifier cannot be empty") } if factory == nil { return fmt.Errorf("factory for product %q cannot be nil", product) } factoriesMutex.Lock() defer factoriesMutex.Unlock() if _, exists := factories[product]; exists { return fmt.Errorf("product %q is already registered", product) } factories[product] = factory return nil } // NewDeviceCollector creates the registered collector for a configured product. func NewDeviceCollector(config configuration.DeviceConfiguration, client shelly.StatusClient) (prometheus.Collector, error) { product := normalizeProduct(config.Product) factoriesMutex.RLock() factory, exists := factories[product] factoriesMutex.RUnlock() if !exists { return nil, fmt.Errorf("unsupported Shelly product %q (supported: %s)", config.Product, strings.Join(SupportedProducts(), ", ")) } config.Product = product return factory(config, client) } // SupportedProducts returns all registered identifiers in stable order. func SupportedProducts() []string { factoriesMutex.RLock() defer factoriesMutex.RUnlock() products := make([]string, 0, len(factories)) for product := range factories { products = append(products, product) } sort.Strings(products) return products } func normalizeProduct(product string) string { var normalized strings.Builder separator := false for _, character := range strings.ToLower(strings.TrimSpace(product)) { if unicode.IsLetter(character) || unicode.IsDigit(character) { if separator && normalized.Len() > 0 { normalized.WriteByte('_') } normalized.WriteRune(character) separator = false } else { separator = true } } return normalized.String() }