Add PMIC temperature reading (vcgencmd measure_temp pmic) - #164
Merged
Merged
Conversation
The Pi 4/5 carry a Power-Management IC with its own temperature sensor, the only genuinely additional on-board sensor available without extra hardware: `temperature` and `gpu_temperature` both read the single SoC die sensor, and Raspberry Pi OS does not expose the PMIC through sysfs hwmon, so the hwmon collector cannot pick it up either. Read it via `vcgencmd measure_temp pmic` and expose it as an additive, optional `pmic_temperature` object in the snapshot (plus the matching Prometheus gauge and the dashboard's Temperature card). The output form is identical to plain `measure_temp`, so parseVcgencmdTemp is reused; it now returns a plain float64 so neither reading has to borrow the other's type. Boards without the sensor answer with an error line or a non-zero exit, which is treated as "not present" — the field is omitted and neither collection nor the GPU/SoC reading is disturbed. WorstCaseTickOverhead grows from 2x to 3x vcgencmdTimeout: the added call is a third sequential firmware invocation per fast tick, and leaving the budget at two would let /healthz flap on a Pi whose only problem is slow firmware. A new test counts the invocations a tick makes rather than trusting that constant's comment.
Addresses the two non-blocking findings from PR #164's review round 1: - TemperatureCollector.pmicUnsupported latches true once `vcgencmd measure_temp pmic` answers with something that isn't a temperature reading (a board with no PMIC sensor, Pi 3 and earlier). That is a permanent condition for the life of the process, so a board without one no longer pays a pointless vcgencmd invocation on every fast tick forever. A real exec/timeout failure does not latch: it's transient and worth retrying, same as the GPU/SoC reading. parseVcgencmdTemp signals this via a new sentinel, errVcgencmdUnsupportedOutput, so Collect can tell "ran and answered, just not with a reading" apart from "didn't run at all" using errors.Is. Deliberately not extended to the GPU/SoC reading or to ThrottledCollector's get_throttled: neither ever answers "unsupported" on real Pi hardware (vcgencmd's own detection layer already covers "vcgencmd absent" for both), so latching them would add code for a state that can't occur. - TestParseVcgencmdTemp and TestTemperatureCollector_Collect_PMIC are now table-driven (docs/TESTS.md) instead of four and three near-identical top-level functions. Two new tests cover the latch itself: one counts vcgencmd invocations across three Collect() calls to confirm only the first one actually runs the PMIC subcommand, the other confirms a non-zero exit never sets the latch.
Round-2 review of the PMIC latch found that its one behavior-changing line (a temp= prefix followed by a number strconv can't parse) was never exercised by any test, even though it now permanently disables the PMIC reading for the process's lifetime, same as the "error=" response. Add that case to TestParseVcgencmdTemp's table, which brings parseVcgencmdTemp to 100% coverage. Also softened the pmicUnsupported field comment: the latch actually fires on any unrecognized vcgencmd output, not only its "error=1" response, so "the board itself has no PMIC sensor" is now phrased as "in practice" rather than as a categorical guarantee.
SonarCloud flagged TestTemperatureCollector_Collect_PMIC's cognitive complexity at 19 against the default threshold of 15: it counts a t.Run closure's branching into its enclosing function, and the for-loop-plus-closure combination from the table-driven refactor crept past that. Move the closure's body into a standalone helper, assertPMICCollect, called with one line from the closure; behavior and assertions are unchanged.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Pull Request
📖 Description
Adds the Raspberry Pi 4/5 Power-Management IC temperature as an optional,
additive reading — a feature, not a fix, and not a breaking change.
temperatureandgpu_temperatureboth read the single SoC die sensor (CPUand GPU share the die), so the PMIC is the only genuinely additional on-board
sensor available without extra hardware, and it is useful for spotting
power-delivery / board-level heat distinct from CPU load. Raspberry Pi OS does
not expose it through sysfs
hwmon, so the hwmon collector (D7) cannot pick itup — this is the
vcgencmdpath the two issues split between them.What changed:
internal/collector/temperature.gorunsvcgencmd measure_temp pmicinaddition to the plain
measure_temp, reusing the existing runner and parser.The two readings are independent: a board that answers one but not the other
(a Pi 3 has no PMIC) still reports the one it has, and neither failure fails
collection.
parseVcgencmdTempnow returns(float64, error)instead of(GPUTemperature, error), so the PMIC reading does not have to borrow theGPU type's name.
GPUTemperatureis still constructed inCollect, so thegpu_temperatureJSON shape is untouched.vcgencmdRunner.runbecame variadic to carry thepmicargument — still afixed argv, no shell, and the explicit empty child environment is preserved.
internal/collector/types.go/collector.go: newPMICTemperaturetype andan
omitemptypmic_temperatureobject on the snapshot, wired into the fasttick next to the GPU reading.
WorstCaseTickOverheadgrows from2xto3xvcgencmdTimeout. This is theone non-obvious consequence: a fast tick now makes three sequential firmware
calls, and leaving the budget at two would let
/healthzflap on a Pi whoseonly problem is slow firmware. The default
/healthzstaleness bound widensfrom 39 s to 49 s at a 5 s poll interval, for every board.
pimonitor_pmic_temperature_celsiuson/metrics, aPMIC:entry in the dashboard's Temperature card sub-line, and
docs/API.md,docs/ARCHITECTURE.md,README.md.da1e44d,2ce3dfe): apmicUnsupportedlatch onTemperatureCollectorso a board without a PMIC sensor stops re-invokingvcgencmd measure_temp pmicon every fast tick once it has answered withsomething other than a temperature reading (a real exec/timeout failure
stays unlatched and keeps retrying, since that's transient). The three
PMIC-specific
Collect()tests, and the fourparseVcgencmdTemptests, arenow table-driven per
docs/TESTS.md, plus two new tests for the latchitself (it engages after exactly one invocation; a failed exec never
engages it).
🎫 Issues
Closes #56
👩💻 Reviewer Notes
Hardware verification is still outstanding. No test here touches real Pi
firmware, and two claims cannot be checked off-Pi:
vcgencmd measure_temp pmicreturnstemp=NN.N'Con a Pi 4/5 — thefixtures assume the issue's description is accurate.
unknown argument. If some firmware ignores it and returns the SoC
temperature instead,
pmic_temperaturewould carry the die reading on thatboard. Both new failure-path tests assume the error behaviour. A smoke test
on a real Pi 4/5 (expect a PMIC value, typically a few degrees above the
die) and, if possible, a Pi 3 (expect the field to be absent, with
gpu_temperaturestill present) would settle both.Focus areas: the
parseVcgencmdTempsignature change and its callers; theWorstCaseTickOverheadreasoning above; and thepmicUnsupportedlatch addedin the follow-up commits.
This change went through three in-session
pimonitor-reviewerrounds beforeeach push:
89b13e5): 0 blocking / 2 non-blocking — no latch foran unsupported PMIC sensor, and three near-identical PMIC tests that should
have been table-driven. Recorded below at the time as deliberately deferred.
da1e44d, addressing both of the above): 1 blocking /1 non-blocking — the one line whose behavior actually changed (a
temp=prefix followed by a number
strconvcan't parse, which now also latches)had no test reaching it; and the
pmicUnsupportedfield comment overclaimedthat the latch fires only when "the board has no PMIC", when it actually
fires on any unrecognized output.
2ce3dfe, fixing both): clean pass, independentlyverified by coverage diff (
parseVcgencmdTemp90.0% → 100.0%) and amutation check (removing the new sentinel wrap makes only the new test row
fail).
📑 Test Plan
All fixture-based per
docs/TESTS.md— no real/proc,/sysor firmwareaccess. The existing
writeFakeVcgencmdhelper is reused; a newpmicAwareVcgencmdvariant answersmeasure_temp pmicdifferently frommeasure_tempso both sensors can be simulated independently.TestParseVcgencmdTemp(table-driven) — the reused parser decodes a PMICtemp=52.1'Cfixture, and rejects three non-reading shapes (error=1 error_msg=..., garbage output, and atemp=prefix with an unparseablenumber) as
errVcgencmdUnsupportedOutput.TestTemperatureCollector_Collect_PMIC(table-driven) — a real PMICreading, an "unsupported" error line, and a non-zero exit: in every case the
GPU/SoC reading and overall collection survive, only the PMIC field's
presence differs.
TestTemperatureCollector_Collect_PMICUnsupportedLatches— once vcgencmdanswers "unsupported" for
pmic, three subsequentCollect()calls invokethe binary exactly once in total.
TestTemperatureCollector_Collect_PMICTransientFailureKeepsRetrying— anon-zero exit never latches, since the same board might still have a PMIC.
_Collectand_VcgencmdExecFailsextended to assert the PMICfield is absent off-Pi and when the binary fails.
TestVcgencmdRunner_Run_PassesSubcommandArguments—pmicreaches thebinary as its own argv entry.
TestCollector_FastTick_PMICTemperature— the reading reaches the publishedsnapshot.
TestWorstCaseTickOverhead_CoversEveryVcgencmdInvocation— counts thevcgencmd invocations a fast tick actually makes and requires the constant to
budget for all of them, so a future firmware call cannot silently break
/healthz.TestAppJS_RendersPMICTemperatureguards the dashboard line.go build ./...,go vet ./...andgo test ./... -race -coverpass locally(collector 92.0%, httpapi 98.5%).
golangci-lint runcould not be executed inthis environment — the installed binary is built against go1.25 while the repo
targets go1.26.7, so it refuses to load the config. CI covers it.
✅ Checklist
General
go test ./... -race -coverpasses locally).go vet ./...andgolangci-lint runare clean. —go vetis clean;golangci-lintcould not run locally (toolchain mismatch, see Test Plan), left to CI.ARCHITECTURE.mdif this changes a documented design decision.REST API / configuration / packaging
docs/API.mdto reflect a REST API change./api/v1/...response shapes, or a new API version (/api/v2/...) was introduced instead.README.md/packaging/pimonitor.example.yamlto reflect a new or changed configuration option. — README updated; no new config option, so the example YAML is unchanged.packaging/install.shor the systemd units if this changes installation/packaging, and kept the unprivileged/privileged service split intact (seeSECURITY.md). — no packaging change needed:vcgencmdwas already in the service's permitted shell-out set, and no new privilege is required.⏭ Next Steps
Both items originally listed here (the missing "stop trying" latch, and
table-driven tests) were addressed in the follow-up commits described above.
Nothing outstanding beyond the hardware verification called out in Reviewer
Notes.