From e4c2934bdc5f3e1f50edc07ff3cdf5fe970a9e9c Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Mon, 31 Aug 2026 13:54:21 -0400 Subject: [PATCH] fix: poll remote multi-site controllers by internalReference Remote discovery stored Integration display names, so extra sites were dropped when checkSites compared them to legacy Site.Name. Use unifi v6.1.0 InternalReference instead. Co-authored-by: Cursor --- go.mod | 2 +- go.sum | 4 +- pkg/inputunifi/remote.go | 58 ++++++++++++++++++++++++---- pkg/inputunifi/remote_test.go | 72 +++++++++++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 11 deletions(-) create mode 100644 pkg/inputunifi/remote_test.go diff --git a/go.mod b/go.mod index 2fb92ae3..066f029b 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/prometheus/common v0.70.1 github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.12.1 - github.com/unpoller/unifi/v6 v6.0.3 + github.com/unpoller/unifi/v6 v6.1.0 go.opentelemetry.io/otel v1.46.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.46.0 diff --git a/go.sum b/go.sum index 8291259e..a51cd07a 100644 --- a/go.sum +++ b/go.sum @@ -121,8 +121,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= -github.com/unpoller/unifi/v6 v6.0.3 h1:NoxbSA5HLMErs/1yVkmuwm+lcyy9SxSfOLDW58PKqrQ= -github.com/unpoller/unifi/v6 v6.0.3/go.mod h1:d7dz1cBxVbbFZobNRcdUHHYtTdicVyZ05J5OmgoINa8= +github.com/unpoller/unifi/v6 v6.1.0 h1:VDUZSCkK5BS3qF39MgTsJa1plwqZ4sIqBa+JYVamLnE= +github.com/unpoller/unifi/v6 v6.1.0/go.mod h1:d7dz1cBxVbbFZobNRcdUHHYtTdicVyZ05J5OmgoINa8= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= diff --git a/pkg/inputunifi/remote.go b/pkg/inputunifi/remote.go index c5b2d1b8..f05ee105 100644 --- a/pkg/inputunifi/remote.go +++ b/pkg/inputunifi/remote.go @@ -175,13 +175,9 @@ func (u *InputUnifi) discoverRemoteControllers(apiKey string) ([]*Controller, er controller.SaveProtectDevices = &f } - // Extract site names - siteNames := make([]string, 0, len(sites)) - for _, site := range sites { - if site.Name != "" { - siteNames = append(siteNames, site.Name) - } - } + // checkSites / getFilteredSites match against legacy Site.Name, which is + // RemoteSite.InternalReference, not the display Name. See unpoller/unpoller#986. + siteNames := remoteSitePollNames(sites) // 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 @@ -208,8 +204,54 @@ func (u *InputUnifi) discoverRemoteControllers(apiKey string) ([]*Controller, er controller.ID = console.ID controllers = append(controllers, controller) - u.Logf("Discovered console %s with %d site(s): %v", consoleName, len(sites), siteNames) + u.Logf("Discovered console %s with %d site(s): %v", consoleName, len(sites), formatRemoteSites(sites)) } return controllers, nil } + +// remoteSitePollNames returns the legacy site identifiers used by checkSites +// and getFilteredSites. Prefer InternalReference (OpenAPI Site overview); +// fall back to Name when the field is missing (older firmware). +func remoteSitePollNames(sites []unifi.RemoteSite) []string { + names := make([]string, 0, len(sites)) + + for _, site := range sites { + name := site.InternalReference + if name == "" { + name = site.Name + } + + if name != "" { + names = append(names, name) + } + } + + return names +} + +// formatRemoteSites logs display name plus legacy id when they differ. +func formatRemoteSites(sites []unifi.RemoteSite) []string { + out := make([]string, 0, len(sites)) + + for _, site := range sites { + id := site.InternalReference + if id == "" { + id = site.Name + } + + if id == "" { + continue + } + + if site.Name != "" && !strings.EqualFold(site.Name, id) { + out = append(out, site.Name+" ("+id+")") + + continue + } + + out = append(out, id) + } + + return out +} diff --git a/pkg/inputunifi/remote_test.go b/pkg/inputunifi/remote_test.go new file mode 100644 index 00000000..0c981d10 --- /dev/null +++ b/pkg/inputunifi/remote_test.go @@ -0,0 +1,72 @@ +package inputunifi + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/unpoller/unifi/v6" +) + +func TestRemoteSitePollNames(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + sites []unifi.RemoteSite + want []string + }{ + { + name: "multi-site uses internalReference not display name", + sites: []unifi.RemoteSite{ + {ID: "uuid-1", InternalReference: "default", Name: "Default"}, + {ID: "uuid-2", InternalReference: "abc1def2", Name: "Office"}, + }, + want: []string{"default", "abc1def2"}, + }, + { + name: "falls back to Name when internalReference is empty", + sites: []unifi.RemoteSite{ + {ID: "uuid-1", Name: "default"}, + }, + want: []string{"default"}, + }, + { + name: "skips sites with neither field", + sites: []unifi.RemoteSite{ + {ID: "uuid-1"}, + {ID: "uuid-2", InternalReference: "site2", Name: "Two"}, + }, + want: []string{"site2"}, + }, + { + name: "empty list", + sites: nil, + want: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tt.want, remoteSitePollNames(tt.sites)) + }) + } +} + +func TestFormatRemoteSites(t *testing.T) { + t.Parallel() + + sites := []unifi.RemoteSite{ + {ID: "uuid-1", InternalReference: "default", Name: "Default"}, + {ID: "uuid-2", InternalReference: "abc1def2", Name: "Office"}, + {ID: "uuid-3", InternalReference: "only-id"}, + {ID: "uuid-4", Name: "legacy-name-only"}, + } + + assert.Equal(t, []string{ + "default", + "Office (abc1def2)", + "only-id", + "legacy-name-only", + }, formatRemoteSites(sites)) +}