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 <cursoragent@cursor.com>
This commit is contained in:
Cody Lee
2026-08-31 13:54:21 -04:00
co-authored by Cursor
parent 6972dfc0c2
commit e4c2934bdc
4 changed files with 125 additions and 11 deletions
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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=
+50 -8
View File
@@ -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
}
+72
View File
@@ -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))
}