Prefer a known stats channel and fall back to airQuality for temperature
and humidity. Omit channels whose status is unknown, and add the
particulate and gas series only when the controller includes them.
Co-authored-by: Cursor <cursoragent@cursor.com>
interval=0 now turns off the Prometheus scrape cache as PR #1014
documented, and sub-15s intervals warn instead of clamping (#1083).
Adopted devices stay in Prometheus, Influx, OTel, and Datadog exports
while locate/identify is on (#1075).
Co-authored-by: Cursor <cursoragent@cursor.com>
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>
Remote API discovery was unconditionally overwriting default_site_name_override
with the console name for Cloud Gateways. Only apply the console name fallback
when the user has not already configured an override.
Fixes#1057
Co-authored-by: Cursor <cursoragent@cursor.com>
Add v3 integration and version tests, migration notes, InfluxDB 3 docker-compose stack, and README updates to finish the remaining plan phases.
Co-authored-by: Cursor <cursoragent@cursor.com>
Introduce explicit version selection with the influxdb3-go client alongside existing v1 and v2 paths, and resolve overlapping tag/field keys required for InfluxDB 3 write validation.
Co-authored-by: Cursor <cursoragent@cursor.com>
GoReleaser's make man hook failed because go get no longer works
outside a module. Replace deprecated go get with go install for
md2roff and rsrc.
Co-authored-by: Cursor <cursoragent@cursor.com>
golangci-lint v2.9 is built with Go 1.26 and panics when type-checking
dependencies that include go1.27-only source files.
Co-authored-by: Cursor <cursoragent@cursor.com>
Collects Protect device data (sensors, cameras, lights, bridges, link
stations, NVR) via the official Integration API and exports it through
Prometheus, InfluxDB, and DataDog. Opt-in via save_protect_devices,
gated by protect_api_key.
Bumps github.com/unpoller/unifi to v6, which added the Protect API
client (breaking change: FlexInt/FlexBool/FlexFloat replace nullable
pointers).
`disable = false` is a double negative, and a bool named disable cannot
express opt-in anyway: it zero-values to false, so the flag was inert and
opt-in rested entirely on the device list being empty.
`enable` defaults to false and is now the real gate -- Initialize, Metrics
and DebugInput all return early unless it is set. The two existing guards
remain: an empty device list is still a no-op, and no default URL is ever
synthesized.
Configuring devices while enable is false is always a mistake, so that
combination logs one error instead of silently collecting nothing.
Adds binding tests for the flag across toml, json, yaml and UP_UNAS_ENABLE
(the env name derives from the xml tag, not the json one), plus a test that
all three shipped examples default to off. Both were verified by mutation:
breaking a struct tag or flipping an example fails the suite.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AppendMetrics merges every slice field on Metrics except SpeedTests, so
speed test results were discarded on the way from the input to the
outputs. Both call sites start from a non-nil &Metrics{}, so this hit
every user on every poll: inputunifi collected the results, and the
export code in promunifi, influxunifi and datadogunifi was dead.
TS was dropped the same way. The aggregate starts bare and nothing
restored the timestamp, so it stayed zero -- and influxunifi's collect()
stamps any point that carries no timestamp of its own with the
aggregate's, which meant the zero time. Both Influx clients omit a zero
timestamp and let the server assign one, so the damage was limited to
points being stamped on arrival rather than at poll time, but it made
the fallback path meaningless. First writer wins: the earliest input's
timestamp is the one that describes the batch.
The failure mode here is what makes it worth guarding rather than just
patching. A new metric family needs a field on Metrics and an append
line in AppendMetrics, and omitting the second loses every metric in
that family with no error, no log line, and a passing build.
TestAppendMetricsCoversEverySliceField walks the struct by reflection
and fails naming any slice field that is not merged, so a new field is
covered the moment it is declared rather than when someone notices the
graphs are empty.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds a new `unas` input plugin that polls UNAS Pro storage consoles and
exports console health, storage pools, disks and shares to Prometheus,
InfluxDB and DataDog.
UNAS is a separate plugin rather than a device type inside inputunifi
because a storage-only console has no Network application: it cannot
answer /status, has no sites, and shares none of the UniFi device schema.
The plugin is opt-in and inert until an operator names a console. Opt-in
is expressed as "no devices configured" rather than a `disable` flag,
because a bool named `disable` zero-values to false and so cannot make a
plugin default-off. Initialize returns silently on an empty device list
and, unlike inputunifi, nothing synthesizes a default URL.
Two behaviours are worth calling out for reviewers:
- Metrics returns (metrics, nil) whenever any console was collected.
poller.collectMetrics uses `if err != nil {} else if metric != nil`,
so returning both would discard every healthy console because one
failed. Only a total failure returns an error.
- Re-auth fires on total failure, not on a 401. A mid-session 401 from
GetData surfaces as ErrInvalidStatusCode, not ErrAuthenticationFailed,
so there is no sentinel to match on. Session expiry fails all four
endpoints at once, which is exactly the total-failure case.
Prometheus metrics use the `unifi_unas_` prefix, which diverges from the
`unas_` prefix used by the reference implementation; dashboards built
against that will need query edits.
Requires unifi/v5 v5.31.0 for the UNAS client and structs.
Credit to alexgreenbank/unaspoller for mapping the endpoints.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Some Network 10.x+ controllers (UniFi OS, Network 10.5.67 confirmed)
return HTTP 400 api.err.InvalidObject from list/alarm instead of a 404
when the endpoint is gone, so collectAlarms fell through the existing
ErrEndpointNotFound skip and logged a real ERROR on every poll.
Fixes#1050
BACKUP and DISCONNECTED WAN interfaces both reported as 0, making
them indistinguishable in Prometheus (fixes#1045). InfluxDB and
DataDog already expose the raw state; this brings Prometheus in
line by adding a state label alongside the existing 1/0 gauge.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
InputUnifi.Events returns (nil, nil) when disabled, but collectEvents
dereferenced e.Logs unconditionally after a successful (err == nil)
call, crashing the poller. Same crash class as #1030, found while
verifying the recover-based fix.
Replaces the sent-bool guard duplicated across three goroutines with a
small per-call helper (recoverInitialize/recoverEvents/recoverMetrics)
that wraps the plugin call and converts a panic into a returned error.
Each goroutine then always sends its result exactly once, so there's
no risk of a double-send deadlocking the collector.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Metrics/Events/Initialize each fan out to input plugins in their own
goroutines with no recover(), so a panic there (e.g. a UniFi
controller returning an unexpected Site Speed Test aggregated-dashboard
payload) crashes the whole process with exit code 2. Because the
panic occurs in a child goroutine, promunifi's existing safeRefresh
recover() in the caller's goroutine never sees it, which is why the
crash survived the earlier robustness work. This converts a panicking
input into a logged/returned error so polling continues instead of
crashing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Tags pushed with the default GITHUB_TOKEN don't trigger other
workflows' push events, so release.yml never ran after tag.yml
created a new tag. Add workflow_dispatch to release.yml and have
tag.yml call `gh workflow run release.yml --ref <tag>` right after
pushing the tag.
Adds a workflow_dispatch workflow that lets maintainers bump the
patch, minor (default), or major version using mdomke/git-semver,
then tags and pushes the result to trigger the existing release
workflow.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Decouples Prometheus scrape cadence from upstream UniFi API calls so a
429 backoff loop on the controller side no longer stalls /metrics. The
output plugin now owns a 60s background poller (configurable) whose
result is served from an in-memory cache. Concurrent /scrape requests
for the same target are coalesced via singleflight to prevent a noisy
scraper from multiplying upstream load.
Adds two new metrics so operators can detect cache staleness and
refresh failures independently:
- unpoller_prometheus_cache_age_seconds
- unpoller_prometheus_refresh_failures_total
Background goroutine recovers from panics so a malformed input payload
no longer silently kills refreshes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The v3.1.2 release failed because homebrew_casks filters archives by
goarch=[amd64 arm64], but universal_binaries.replace=true collapses the
darwin amd64+arm64 builds into a single darwin/all archive — which the
cask filter rejects.
Drop the universal_binaries block. The unpoller-mac build still produces
darwin amd64 and darwin arm64 separately, and the cask resolves them
into per-arch on_intel/on_arm blocks. Net effect on release artifacts:
the single universal macOS archive is replaced by two per-arch archives.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Rename archives.builds/nfpms.builds to ids; convert format/format_overrides.format to formats arrays.
- Collapse three dockers + six docker_manifests entries into one dockers_v2 block; update Dockerfile to use $TARGETPLATFORM for multi-arch buildx.
- Migrate brews to homebrew_casks (binary->binaries, url_template->url.template, install/post_install->hooks.post.install). Cask is macOS-only, so ids is reduced to unpoller-mac.
Per-arch image tags (e.g. :latest-amd64, :latest-arm64v8, :latest-armv7) are no longer published; only the multi-arch manifest tags (latest, v{Major}, full version) remain. Linuxbrew install path is dropped; Linux users should use the deb/rpm or Docker image.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Picks up the gzip decompression fix for the Site Manager remote API
(unpoller/unifi#220), which restores remote-API discovery for Official
UniFi Cloud Hosted consoles whose responses are gzip-compressed but
served without a Content-Encoding header.
Closes#997
Bumps github.com/unpoller/unifi/v5 from v5.26.0 to v5.27.0 along with
golang.org/x/crypto, term, sys, and text to their latest releases.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Influx and Datadog integration tests assert that the captured field/gauge
sets exactly match the YAML. Add the new uap uplink_* entries so the
TestInfluxV1Integration and Datadog integration tests stay green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Exposes the uplink medium (wire vs wireless) and link speed for UniFi access
points so users can detect when an AP downgrades from gigabit to fast ethernet,
which was the original ask in #988. UAPs previously had zero uplink coverage
in any output plugin; now influxunifi, datadogunifi, and promunifi all report
uplink_type, uplink_speed, uplink_max_speed, and related fields.
Also brings Prometheus to parity with Influx/Datadog by emitting uplink
metrics for USW, UBB, and UDB devices (previously only USG/UDM/UXG had them
in promunifi). A new exportDeviceUplink helper in promunifi/usg.go reuses
the existing unpoller_device_uplink_* descriptors to avoid descriptor
collision (per c48b9917).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes#1001. Mirrors the DataDog plugin's global tags feature for
InfluxDB. Per-metric tags take precedence on key collision so
site/device identifiers can never be overwritten by a misconfigured
global. Configurable via TOML/JSON/YAML under influxdb.tags.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Homebrew formula's install/post_install blocks referenced
examples/up.conf, but the source tarball ships examples/up.conf.example,
causing brew install to fail with ENOENT. Update both references to use
the correct filename, matching how the rest of .goreleaser.yaml already
sources this file.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds 21 new data types from unifi v5.26.0 across all metric output plugins
(InfluxDB, Prometheus, DataDog). Per-site Integration/v1 calls are gated on
API key configuration and only run for user-configured sites; ErrEndpointNotFound
is handled gracefully so older firmware continues to work without log spam.
Also migrates events collection (collectAlarms, collectAnomalies, collectEvents,
collectIDs, collectProtectLogs) to handle Network 10.x+ endpoint removals via
ErrEndpointNotFound, with debug-level logging to avoid per-poll noise.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updated the install section in the brews configuration to use the
new Homebrew formula syntax for directory creation.
Changes:
- Changed `etc.mkdir "unpoller"` to `(etc/"unpoller").mkpath`
The old syntax was causing errors with newer Homebrew versions:
TypeError: no implicit conversion of String into Integer
This fix ensures compatibility with Homebrew 4.3+ while maintaining
backward compatibility with older versions.
Fixes#742
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Added device name enrichment to alarms so that Loki logs show
human-readable device names instead of just MAC addresses.
Changes:
- Modified collectAlarms to fetch devices and build MAC-to-name lookup
- Added extractDeviceNameFromAlarm helper to extract MAC addresses from
alarm messages and lookup corresponding device names
- Device names are extracted from messages like "AP[fc:ec:da:89:a6:91]"
or from SrcMAC/DstMAC fields
- Added go.mod replace directive to use local unifi library with new
DeviceName field
The device_name field will now be included in the JSON output sent to
Loki, making it easier to identify which device triggered an alarm.
Fixes#415
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixes#417
UniFi controllers populate RemoteUserNumActive for VPN connections but
leave NumUser at 0 for the VPN subsystem. This caused dashboard queries
looking for num_user in the VPN subsystem to always show 0 active users,
even when VPN connections were active.
Root Cause:
For most subsystems (wlan, lan, www), the controller populates NumUser
directly. However, for the VPN subsystem, the controller uses the
RemoteUserNumActive field instead, leaving NumUser at 0.
The Prometheus exporter had special handling for VPN (lines 148-156 in
pkg/promunifi/site.go) and exported RemoteUserNumActive, but did not
export NumUser. The InfluxDB and Datadog exporters exported all fields
for all subsystems without special handling, resulting in num_user
always being 0 for VPN.
Existing Grafana dashboards query:
SELECT "num_user" FROM "subsystems" WHERE subsystem='vpn'
This always returned 0 even with active VPN users.
Solution:
For all three exporters (InfluxDB, Datadog, Prometheus), when the
subsystem is 'vpn' and NumUser is 0 but RemoteUserNumActive has a
value, populate num_user with RemoteUserNumActive.
Changes:
- pkg/influxunifi/site.go: Add VPN-specific num_user fallback logic
- pkg/datadogunifi/site.go: Add VPN-specific num_user fallback logic
- pkg/promunifi/site.go: Add NumUser metric to VPN case with fallback
This maintains backward compatibility - existing queries for num_user
will now work correctly, and the remote_user_num_active field is still
available for those who updated their dashboards.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixes#425
When polling multiple controllers, if one controller was down or
unreachable, unpoller would stop collecting data from ALL controllers.
This caused complete data loss across all sites when just one was down.
Root Cause:
Both Metrics() and Events() methods would immediately return an error
when any controller failed, skipping all remaining controllers in the
loop.
Changes:
- Log errors from failed controllers but continue to next controller
- Track collection errors separately from successful data collection
- Only return error if ALL controllers failed and no data was collected
- Return success if at least one controller provided data
This allows unpoller to continue monitoring healthy controllers even
when some are temporarily unreachable due to network issues, timeouts,
or maintenance.
Example behavior:
- Controller 1: Down (timeout) - logs error, continues
- Controller 2: Up - collects data successfully
- Controller 3: Up - collects data successfully
- Result: Returns data from controllers 2 and 3
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixes#904
When a poll fails (typically with 401 Unauthorized after ~2 hour token
expiration), the code would re-authenticate but then return the original
poll error without retrying. This caused a one-minute data gap every
2 hours.
Changes:
- After successful re-authentication, retry the poll operation
- Add 500ms delay before retry to allow controller to process new auth
- Rename error variable to avoid shadowing during re-auth attempt
This ensures that transient authentication failures during the re-auth
window don't cause data gaps.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Ports providing PoE power are no longer considered "dead" even when
disabled or down. This allows users to collect PoE metrics from ports
that are disabled for security reasons but still providing power.
Fixes#910
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Adds log_unknown_types config option (default: false) to control logging
of unknown UniFi device types. When disabled (default), unknown devices
are silently ignored to reduce log volume. When enabled, they are logged
as DEBUG messages instead of ERROR. Addresses issue #912.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Track the number of bytes written per request for both InfluxDB and Prometheus outputs.
InfluxDB:
- Added bytesT counter constant
- Implemented calculateMetricBytes() to estimate line protocol size
- Updated batchV1() and batchV2() to count bytes per point
- Updated log output to display bytes written
Prometheus:
- Added Bytes field to Report struct
- Updated export() to calculate approximate metric byte size
- Updated log output to display bytes written
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Speed tests were not being reported correctly for multi-WAN setups
because the device-level speedtest-status field was returning zeros.
The data has moved to a new aggregated dashboard API endpoint.
Changes:
- Add GetSpeedTests() and GetSiteSpeedTests() methods to fetch from
/v2/api/site/{site}/aggregated-dashboard endpoint
- Create SpeedTestResult data structures to capture per-WAN metrics
- Update Prometheus exporter with new speedtest_* metrics per interface
- Update InfluxDB exporter to write speedtest measurements per WAN
- Update Datadog exporter with unifi.speedtest.* metrics per WAN
- Update metrics collection to include speed test data for all sites
Metrics now include labels/tags for:
- wan_interface: Physical interface (eth8, eth9, etc.)
- wan_group: Logical WAN name (WAN, WAN2, etc.)
- site_name: Site identifier
- source: Controller URL
Gracefully handles older controllers without the new API endpoint.
Fixes#841🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This change significantly expands the metrics exported for UBB devices
to InfluxDB and Datadog, matching the comprehensive coverage added to
the Prometheus output.
Changes to InfluxDB (pkg/influxunifi/ubb.go):
- Added batchUBBstats() to export comprehensive statistics separated
by radio (total, wifi0, terra2, user-wifi0, user-terra2)
- Added VAP table export via processVAPTable()
- Added Radio table export via processRadTable()
- Added P2P stats (rx_rate, tx_rate, throughput)
- Added link quality metrics (link_quality, link_quality_current,
link_capacity)
- Comprehensive stats exported to new "ubb_stats" table with full
breakdown of traffic per radio
Changes to Datadog (pkg/datadogunifi/ubb.go):
- Added batchUBBstats() to export comprehensive statistics separated
by radio (total, wifi0, terra2, user-wifi0, user-terra2)
- Added VAP table export via processVAPTable()
- Added Radio table export via processRadTable()
- Added P2P stats (rx_rate, tx_rate, throughput)
- Added link quality metrics (link_quality, link_quality_current,
link_capacity)
- Comprehensive stats exported with namespace "ubb.stats"
All implementations now fully support:
- 5GHz radio (wifi0) metrics
- 60GHz radio (terra2/ad) metrics - Full 802.11ad support!
- Per-radio RX/TX packets, bytes, errors, dropped, retries
- User-specific metrics for each radio
- Interface-specific metrics (ath0 for 5GHz, wlan0 for 60GHz)
- Point-to-point link statistics and quality metrics
Fixes: #409🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This change significantly improves UniFi Building Bridge (UBB) device
support by adding comprehensive Prometheus metric exports.
UBB devices are point-to-point wireless bridges with dual radios:
- wifi0: 5GHz radio (802.11ac)
- terra2/wlan0/ad: 60GHz radio (802.11ad - Terragraph/WiGig)
Changes:
- Added exportUBBstats() to export UBB-specific statistics separated
by radio (total, wifi0, terra2, user-wifi0, user-terra2)
- Added exportP2Pstats() to export point-to-point link metrics
(rx_rate, tx_rate, throughput)
- Added VAP (Virtual Access Point) table export via existing exportVAPtable()
- Added Radio table export via existing exportRADtable() to capture
60GHz radio metrics
- Added link quality metrics (link_quality, link_quality_current,
link_capacity)
- Added comprehensive comments documenting UBB device characteristics
and 60GHz band support
The implementation reuses existing UAP metric descriptors where
appropriate, allowing UBB metrics to be collected alongside UAP metrics
in Prometheus with proper labeling for differentiation.
Requires: unpoller/unifi#169 (UBB type definition fixes)
Fixes: #409🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The Docker health check was attempting to bind to ports already in use by
the running application, causing "address already in use" errors. This fix
adds a health check mode that skips network binding operations while still
validating output configuration (listen addresses, paths, etc.).
Changes:
- Add health check mode flag in pkg/poller/outputs.go
- Update prometheus and webserver DebugOutput() to skip port binding in health check mode
- Maintain full configuration validation without network conflicts
Fixes#892🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Implements #406 by adding a --health CLI flag and HEALTHCHECK instruction
to the Dockerfile. This allows Docker and container orchestration platforms
to monitor container health automatically.
Changes:
- Added --health flag that validates configuration and plugin connectivity
- Implemented HealthCheck() method in pkg/poller/commands.go
- Updated Dockerfile with HEALTHCHECK instruction (30s interval, 10s timeout)
- Updated MANUAL.md with --health flag documentation
- Added health check documentation to Docker README
- Added comments to docker-compose examples about built-in health check
The health check:
- Validates configuration file is found and parseable
- Ensures at least one input and one enabled output are configured
- Performs basic validation on enabled outputs
- Returns exit code 0 (healthy) or 1 (unhealthy)
- Runs silently for Docker compatibility
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>