Release tags now fail closed without signing secrets, ship a signed Windows PE via house signerd, and replace per-arch Darwin tarballs with one stapled universal archive.
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>
Bumps the all group with 1 update: [golang.org/x/crypto](https://github.com/golang/crypto).
Updates `golang.org/x/crypto` from 0.54.0 to 0.55.0
- [Commits](https://github.com/golang/crypto/compare/v0.54.0...v0.55.0)
---
updated-dependencies:
- dependency-name: golang.org/x/crypto
dependency-version: 0.55.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: all
...
Signed-off-by: dependabot[bot] <support@github.com>
The Site Speed Test poll against the controller's aggregated-dashboard
endpoint has always run unconditionally, so operators whose controllers
misbehave on that endpoint have no way to skip the request. Every other
optional collection already has a save_* flag; this adds the missing one.
Defaults to true in all three default paths (local defaults,
per-controller defaults and remote discovery) so existing setups keep
behaving exactly as before.
Refs #1030
Co-Authored-By: Claude Opus 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.