feat(web-ui): detect and surface Raspberry Pi under-voltage/throttling#383
Conversation
Adds a vcgencmd get_throttled check to the system-status SSE stream and surfaces it in the web UI: - A header badge (next to CPU/Memory/Temp) that stays hidden when healthy, turns red when under-voltage/throttling is happening right now, and yellow if it happened earlier this session but has since cleared. - A dismissible top banner (same pattern as the update-available banner) that appears while under-voltage/throttling is actively occurring, with guidance to check the power supply. Re-appears on a fresh occurrence even if a previous one was dismissed. - A "Power Supply" card on the Overview tab alongside CPU/Memory/Temp/ Display Status. Motivated by a real device showing intermittent brightness flicker that turned out to be ~1 under-voltage event every 30-90s (visible live via `vcgencmd get_throttled` and dmesg's "Undervoltage detected!" messages) -- there was no way to see this from the web UI, only by SSHing in. Returns None on non-Pi platforms (no vcgencmd on PATH), matching the existing guard pattern used for the CPU temperature read. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 4 |
| Duplication | 0 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
|
Warning Review limit reached
Next review available in: 55 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds Raspberry Pi power/throttling monitoring: the backend parses ChangesPower Monitoring Feature
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant system_status_generator
participant vcgencmd
participant updateSystemStats
participant updatePowerStatus
participant renderPowerInfo
system_status_generator->>vcgencmd: get_throttled
vcgencmd-->>system_status_generator: throttled hex mask
system_status_generator-->>updateSystemStats: status.power
updateSystemStats->>updatePowerStatus: data.power
updateSystemStats->>renderPowerInfo: data.power
updatePowerStatus->>updatePowerStatus: toggle power-stat and warning banner
renderPowerInfo->>renderPowerInfo: build power diagnostics summary
Related Issues: Not specified in the provided changes. Related PRs: Not specified in the provided changes. Suggested labels: enhancement, web-interface Suggested reviewers: ChuckBuilds 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
web_interface/app.py (2)
530-532: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider
warninginstead ofdebugfor unexpected failures.The nearby
systemctlcheck logs failures atwarninglevel (Line 569), but this catch-all logs atdebug, which is easy to miss during remote troubleshooting on headless Pi deployments. As per coding guidelines, "Implement comprehensive logging for remote debugging on Raspberry Pi."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web_interface/app.py` around lines 530 - 532, The catch-all failure logging in the vcgencmd path is too quiet for remote troubleshooting. Update the exception handler around the vcgencmd get_throttled call to use app.logger.warning instead of app.logger.debug, matching the nearby systemctl failure logging and the remote-debugging expectations. Keep the same exception coverage and message context in the relevant vcgencmd helper.Source: Coding guidelines
503-533: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winAdd caching for
_get_power_status()to avoid a subprocess fork every 10s per SSE client.Unlike the
systemctl is-activecheck just above (cached via_ledmatrix_service_cachewith a TTL),_get_power_status()shells out tovcgencmdon every generator loop iteration, once per connected client. With multiple browser tabs/clients this multiplies subprocess forks on a resource-constrained Pi.♻️ Suggested caching pattern (mirrors existing `_ledmatrix_service_cache`)
+_power_status_cache = {'status': None, 'timestamp': 0} +_POWER_STATUS_CACHE_TTL = 10 # seconds + def _get_power_status(): """Check Raspberry Pi under-voltage/throttling status via vcgencmd. ...""" if not _VCGENCMD: return None + now = time.time() + if (now - _power_status_cache['timestamp']) < _POWER_STATUS_CACHE_TTL: + return _power_status_cache['status'] try: result = subprocess.run( [_VCGENCMD, 'get_throttled'], capture_output=True, text=True, timeout=2 ) match = re.search(r'0x([0-9a-fA-F]+)', result.stdout) if not match: + _power_status_cache.update(status=None, timestamp=now) return None bits = int(match.group(1), 16) - return { + status = { 'under_voltage_now': bool(bits & 0x1), ... } + _power_status_cache.update(status=status, timestamp=now) + return status except (subprocess.SubprocessError, OSError, ValueError) as e: app.logger.debug("vcgencmd get_throttled failed: %s", e) + _power_status_cache.update(status=None, timestamp=now) return NoneAs per coding guidelines, "Optimize code for Raspberry Pi's limited RAM and CPU capabilities."
Also applies to: 573-582
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web_interface/app.py` around lines 503 - 533, Add TTL caching to `_get_power_status()` so it does not run `vcgencmd get_throttled` on every SSE loop iteration for each client. Mirror the existing `_ledmatrix_service_cache` pattern used for the `systemctl is-active` check by storing the parsed power-status result plus a timestamp, and return the cached value until the TTL expires. Keep the subprocess call and parsing inside `_get_power_status()`, but make callers reuse the cached result so multiple browser tabs do not multiply forks.Source: Coding guidelines
web_interface/templates/v3/base.html (2)
961-964: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winBanner text is hardcoded to "Under-voltage" even though it can also trigger from
throttled_nowalone.
activeNowisunder_voltage_now || throttled_now(Line 1492), but the static banner copy at Line 963 only mentions under-voltage. If throttling occurs without under-voltage, the message is inaccurate.💬 Suggested dynamic text
- Under-voltage detected right now — the display may flicker. Check your power supply. + <!-- populate dynamically in updatePowerStatus() based on which flag(s) are active -->const bannerText = document.getElementById('power-warning-banner-text'); if (bannerText) { bannerText.textContent = power.under_voltage_now ? 'Under-voltage detected right now — the display may flicker. Check your power supply.' : 'Throttling detected right now — the display may flicker. Check your power supply.'; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web_interface/templates/v3/base.html` around lines 961 - 964, The power warning banner text is hardcoded to under-voltage even though the banner can be shown for throttling as well; update the existing banner text in base.html so it reflects the actual active condition. Use the power state values already available where the banner is updated (around the logic that sets activeNow) and the existing element id power-warning-banner-text to choose between under-voltage and throttling copy, keeping the rest of the message consistent.
954-974: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueBanner/header only surface
under_voltage_now/throttled_now;soft_temp_limit_*andfreq_capped_*from the SSE payload are silently unused.
_get_power_status()(app.py) returnsfreq_capped_now/occurredandsoft_temp_limit_now/occurred, but neither the header stat, banner, nor overview card (Lines 1567-1568 below) reference them. If this is intentional scope-narrowing to under-voltage/throttling only, consider a brief comment noting it; otherwise these flags should feed into the same active/occurred logic.Also applies to: 1483-1521
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web_interface/templates/v3/base.html` around lines 954 - 974, The power warning UI is only using under_voltage_now and throttled_now, while soft_temp_limit_* and freq_capped_* from _get_power_status() are being ignored. Update the banner/header logic in the power warning rendering path (including the banner markup in base.html and the related overview/stat handling) to either incorporate soft_temp_limit_now/occurred and freq_capped_now/occurred into the same active/occurred state handling, or add a brief inline comment in the relevant template/controller code if the reduced scope is intentional. Refer to _get_power_status() and the power-warning-banner / overview card rendering so the behavior stays consistent across all surfaces.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@web_interface/templates/v3/base.html`:
- Around line 1564-1585: The power status update logic in the block that uses
powerStatusEl and powerStatusIconEl only handles truthy data.power, so non-Pi
cases leave the initial placeholder unchanged. Update this rendering path to
explicitly handle a falsy data.power case by setting a clear “Not available” (or
equivalent) status and appropriate styling/icon state, while keeping the
existing under-voltage and OK branches for the current data.power checks.
---
Nitpick comments:
In `@web_interface/app.py`:
- Around line 530-532: The catch-all failure logging in the vcgencmd path is too
quiet for remote troubleshooting. Update the exception handler around the
vcgencmd get_throttled call to use app.logger.warning instead of
app.logger.debug, matching the nearby systemctl failure logging and the
remote-debugging expectations. Keep the same exception coverage and message
context in the relevant vcgencmd helper.
- Around line 503-533: Add TTL caching to `_get_power_status()` so it does not
run `vcgencmd get_throttled` on every SSE loop iteration for each client. Mirror
the existing `_ledmatrix_service_cache` pattern used for the `systemctl
is-active` check by storing the parsed power-status result plus a timestamp, and
return the cached value until the TTL expires. Keep the subprocess call and
parsing inside `_get_power_status()`, but make callers reuse the cached result
so multiple browser tabs do not multiply forks.
In `@web_interface/templates/v3/base.html`:
- Around line 961-964: The power warning banner text is hardcoded to
under-voltage even though the banner can be shown for throttling as well; update
the existing banner text in base.html so it reflects the actual active
condition. Use the power state values already available where the banner is
updated (around the logic that sets activeNow) and the existing element id
power-warning-banner-text to choose between under-voltage and throttling copy,
keeping the rest of the message consistent.
- Around line 954-974: The power warning UI is only using under_voltage_now and
throttled_now, while soft_temp_limit_* and freq_capped_* from
_get_power_status() are being ignored. Update the banner/header logic in the
power warning rendering path (including the banner markup in base.html and the
related overview/stat handling) to either incorporate
soft_temp_limit_now/occurred and freq_capped_now/occurred into the same
active/occurred state handling, or add a brief inline comment in the relevant
template/controller code if the reduced scope is intentional. Refer to
_get_power_status() and the power-warning-banner / overview card rendering so
the behavior stays consistent across all surfaces.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 37eb83fd-e8b5-4278-a0d5-e93565339b51
📒 Files selected for processing (4)
web_interface/app.pyweb_interface/static/v3/app.cssweb_interface/templates/v3/base.htmlweb_interface/templates/v3/index.html
The header badge/banner/Overview card added in the previous commit only show a collapsed "is it bad right now" signal. This adds a "Power Supply" section to the Tools tab with the full 8-flag breakdown (under-voltage, throttled, freq-capped, soft-temp-limit -- each split into "right now" vs "occurred since boot") for actually troubleshooting a recurring issue, plus a pointer to the README's power supply sizing guidance when something is or was flagged. Reuses the existing stats SSE stream (window.statsSource) rather than adding a new endpoint -- the same payload already drives the header/ banner/Overview card, so this just listens for it too. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
- Overview card left its "--" placeholder forever on non-Pi platforms since the update only handled a truthy data.power. Now explicitly renders "Not available" with a neutral icon/color for that case. - vcgencmd failures logged at debug, invisible in default remote log output. Bumped to warning to match the nearby systemctl failure logging. - The banner/badge/card and the Tools summary line only looked at under_voltage_now/throttled_now (+ occurred), silently ignoring freq_capped_now/occurred and soft_temp_limit_now/occurred from _get_power_status() -- a Pi that's actively soft-thermal-limited or frequency-capped showed a green "OK" everywhere except the detailed flag table buried in Tools. All four surfaces now fold all four "now"/ "occurred" flags into the same active/occurred state. - The banner text was hardcoded to "Under-voltage detected..." even when the actual active condition was throttling/freq-capping/thermal limiting. Added _activePowerConditionLabels() (shared, non-module global scope) to build the banner/tooltip text from whichever flags are actually set. Skipped: TTL-caching _get_power_status() to avoid "multiplying forks across browser tabs" -- that premise doesn't hold against this codebase. _StreamBroadcaster (its own docstring says as much) already runs exactly one shared generator per tick regardless of client count, identical to the uncached cpu_temp file-read two lines above it; there's nothing to multiply. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
web_interface/templates/v3/base.html (1)
1518-1521: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value"Occurred earlier" tooltip is generic, unlike the active-now path.
Line 1516 dynamically builds the active-condition label via
_activePowerConditionLabels, but the "occurred earlier" title at Line 1521 is a hardcoded string listing all four condition types regardless of which one(s) actually occurred. Consider building an equivalent "occurred" label list for consistency.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web_interface/templates/v3/base.html` around lines 1518 - 1521, The “occurred earlier” tooltip in the power-status rendering logic is hardcoded and does not match the specific condition(s) that were detected. Update the same status-building path in the template’s power-condition display logic to generate an occurrence label list from the actual condition flags, similar to how the active-now branch uses _activePowerConditionLabels, and use that computed text for the statEl.title in the occurredEarlier branch.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@web_interface/templates/v3/base.html`:
- Around line 1496-1504: The updatePowerStatus function only hides power-stat
when power is falsy, leaving power-warning-banner and power-warning-banner-text
visible with stale content. Update updatePowerStatus in base.html so the falsy
branch also hides the power-warning-banner element (and clears or updates its
text as needed) before returning, keeping the banner state consistent when
_get_power_status() fails transiently.
In `@web_interface/templates/v3/partials/tools.html`:
- Around line 359-372: The status badge in the tools template is using dynamic
Tailwind-style color utilities that are not defined in the shipped stylesheet,
so the badge won’t render with the intended colors. Update the markup in the
summary badge block (and the existing dirty/clean badge nearby) to use only
static classes that already exist in app.css, or add the missing selectors
there; use the badge logic around summaryColor/summaryText as the place to
change.
---
Nitpick comments:
In `@web_interface/templates/v3/base.html`:
- Around line 1518-1521: The “occurred earlier” tooltip in the power-status
rendering logic is hardcoded and does not match the specific condition(s) that
were detected. Update the same status-building path in the template’s
power-condition display logic to generate an occurrence label list from the
actual condition flags, similar to how the active-now branch uses
_activePowerConditionLabels, and use that computed text for the statEl.title in
the occurredEarlier branch.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 14bdee98-cdb8-4760-b8bb-216e5341f105
📒 Files selected for processing (3)
web_interface/app.pyweb_interface/templates/v3/base.htmlweb_interface/templates/v3/partials/tools.html
🚧 Files skipped from review as they are similar to previous changes (1)
- web_interface/app.py
- updatePowerStatus's falsy-power branch only hid power-stat, leaving
power-warning-banner visible with stale text if _get_power_status()
fails transiently. Now hides the banner too and resets the dismissed
flag, same as the "not active" branch.
- The Tools tab's status badge (and the pre-existing dirty/clean badge
right next to it) build class names like bg-${color}-100/text-${color}-800
at runtime. This project hand-rolls its own Tailwind-named utility
classes in app.css rather than running a real Tailwind build, and the
light-mode base rules for bg-red-100/bg-yellow-100/bg-green-100/
text-red-800/text-yellow-800/text-green-800 were simply never defined --
only some had dark-mode overrides, which are no-ops without a base rule
in light mode. Added the missing light-mode bases plus the two missing
dark-mode overrides (bg-yellow-100/bg-green-100), fixing both badges.
- The "occurred earlier" tooltip was a hardcoded string regardless of
which flag(s) actually fired. Generalized _activePowerConditionLabels()
to take a suffix ('_now' or '_occurred') and reused it for both tooltips.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
The Overview tab's "Power Supply" stat card duplicated what the Tools tab's diagnostics section already shows (summary badge + full flag breakdown), so drop the card and its now-dead JS rather than keep two copies in sync. The header badge and warning banner (visible on every page) are unaffected -- only the Overview-tab card is removed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
Summary
vcgencmd get_throttledcheck to the existing system-status SSE stream (system_status_generatorinweb_interface/app.py), decoded intounder_voltage_now/throttled_now/soft_temp_limit_now(+_occurredvariants for "happened earlier this boot, not currently").Noneon non-Pi platforms (novcgencmdonPATH), matching the existing guard pattern already used for the CPU temperature read.Motivation
Diagnosed a real device showing intermittent brightness flicker on the LED panels — turned out to be an active under-voltage condition (
vcgencmd get_throttled→0xd0005, ~1 event every 30-90s perdmesg's "Undervoltage detected!" messages). There was no way to see this from the web UI at all, only by SSHing in and runningvcgencmd/dmesgdirectly. This makes that diagnostic visible to any user without a terminal.Test plan
_get_power_status()bit-decode logic verified against real capturedvcgencmdoutputs (0xd0000historical-only,0xd0005active-now,0x0all-clear)powerdictpower-warning-banner,power-stat,power-status-icon,updatePowerStatus) are present in the HTML_get_power_status()returned a correctly-shaped all-clear dicttest/web_interface/suite passes (2 pre-existing, unrelated failures intest_state_reconciliation.pyconfirmed present onmaintoo, not caused by this change)🤖 Generated with Claude Code
https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
Summary by CodeRabbit