package inputunifi import ( "fmt" "strings" "time" "github.com/unpoller/unifi/v5" ) const discoverMaxAttempts = 3 const discoverRetryDelay = 5 * time.Second // discoverRemoteControllers discovers all controllers via remote API and creates Controller entries. func (u *InputUnifi) discoverRemoteControllers(apiKey string) ([]*Controller, error) { // Handle file:// prefix for API key if strings.HasPrefix(apiKey, "file://") { apiKey = u.getPassFromFile(strings.TrimPrefix(apiKey, "file://")) } if apiKey == "" { return nil, fmt.Errorf("remote API key not provided") } // Use library client client := unifi.NewRemoteAPIClient(apiKey, u.LogErrorf, u.LogDebugf, u.Logf) u.Logf("Discovering remote UniFi consoles...") consoles, err := client.DiscoverNetworkConsoles() if err != nil { return nil, fmt.Errorf("discovering consoles: %w", err) } if len(consoles) == 0 { u.Logf("No Network-capable consoles found via remote API (NVR/Protect/display-only consoles are excluded)") return nil, nil } u.Logf("Found %d Network-capable console(s) via remote API", len(consoles)) var controllers []*Controller for _, console := range consoles { consoleName := console.ConsoleName if consoleName == "" { consoleName = console.ReportedState.Name } if consoleName == "" { consoleName = console.ReportedState.Hostname } if consoleName == "" { consoleName = console.ID } u.LogDebugf("Discovering sites for console: %s (%s)", console.ID, consoleName) var sites []unifi.RemoteSite var lastErr error for attempt := 1; attempt <= discoverMaxAttempts; attempt++ { sites, lastErr = client.DiscoverSites(console.ID) if lastErr == nil { break } if attempt < discoverMaxAttempts { u.LogDebugf("Discover sites attempt %d/%d failed for %s: %v; retrying in %v", attempt, discoverMaxAttempts, consoleName, lastErr, discoverRetryDelay) time.Sleep(discoverRetryDelay) } } if lastErr != nil { u.Logf("Excluding controller %s: api key does not have permissions (after %d attempts)", consoleName, discoverMaxAttempts) continue } if len(sites) == 0 { u.LogDebugf("No sites found for console %s", console.ID) continue } // Create a controller entry for this console // For remote API, the base URL should point to the connector endpoint // The unifi library will append /proxy/network/... paths, so we need to account for that // For remote API with integration endpoints, we set it to the connector base controller := &Controller{ Remote: true, ConsoleID: console.ID, APIKey: apiKey, // Set URL to connector base - the library appends /proxy/network/status // But for integration API we need /proxy/network/integration/v1/... // This may require library updates, but try connector base first URL: fmt.Sprintf("%s/v1/connector/consoles/%s", unifi.RemoteAPIBaseURL, console.ID), } // Ensure defaults are set before calling setControllerDefaults u.setDefaults(&u.Default) // Copy defaults controller = u.setControllerDefaults(controller) // Set remote-specific defaults and ensure all boolean pointers are initialized t := true f := false if controller.VerifySSL == nil { controller.VerifySSL = &t // Remote API should verify SSL } // Ensure all boolean pointers are set (safety check) if controller.HashPII == nil { controller.HashPII = &f } if controller.DropPII == nil { controller.DropPII = &f } if controller.SaveSites == nil { controller.SaveSites = &t } if controller.SaveDPI == nil { controller.SaveDPI = &f } if controller.SaveEvents == nil { controller.SaveEvents = &f } if controller.SaveAlarms == nil { controller.SaveAlarms = &f } if controller.SaveAnomal == nil { controller.SaveAnomal = &f } if controller.SaveIDs == nil { controller.SaveIDs = &f } if controller.SaveTraffic == nil { controller.SaveTraffic = &f } if controller.SaveRogue == nil { controller.SaveRogue = &f } if controller.SaveSyslog == nil { controller.SaveSyslog = &f } if controller.SaveProtectLogs == nil { controller.SaveProtectLogs = &f } if controller.ProtectThumbnails == nil { controller.ProtectThumbnails = &f } // Extract site names siteNames := make([]string, 0, len(sites)) for _, site := range sites { if site.Name != "" { siteNames = append(siteNames, site.Name) } } // For Cloud Gateways, if the only site is "default", use the console name from hosts response // as the default site name override. The console name is in reportedState.name // (consoleName was already set above in the loop) // If we only have one site and it's "default" (case-insensitive), use the console name as override // Note: We keep the actual site name ("default") for API calls, but set the override // for display/metric naming purposes. if len(siteNames) == 1 && strings.EqualFold(siteNames[0], "default") && consoleName != "" { controller.DefaultSiteNameOverride = consoleName // Keep the actual site name for API calls controller.Sites = siteNames u.LogDebugf("Using console name '%s' as default site name override for Cloud Gateway (API will use 'default')", consoleName) } else if len(siteNames) > 0 { controller.Sites = siteNames } else { controller.Sites = []string{"all"} } controller.ID = console.ID controllers = append(controllers, controller) u.Logf("Discovered console %s with %d site(s): %v", consoleName, len(sites), siteNames) } return controllers, nil }