Skip to content

Radiation page: RadiaCode readings (JSON bridge, radwatch, or Home Assistant) - #76

Merged
ThinkOffApp merged 1 commit into
mainfrom
feat/radiation-page
Oct 7, 2026
Merged

ThinkOffApp merged 1 commit into
mainfrom
feat/radiation-page

Conversation

@ThinkOffApp

Copy link
Copy Markdown
Owner

What

A new /radiation page plus /api/radiation JSON, for a RadiaCode 10x. It shows dose rate (µSv/h), count rate (CPS), accumulated dose, battery, temperature, the age of the last reading, and radwatch's watch alarm verdict when that source is used. It follows the /nerd pattern: carwatch/radiation.html is read from disk on each request, behind the same _authorised() gate, and its fetches carry ?t= like /cloudcar. The unified page links to it from its links row.

Sources (radiation.source in config.json)

  • json is the recommended source. A bridge on another machine serves one JSON object over a plain GET with no auth, about every 10 s: dose_rate, count_rate, ts (unix seconds or ISO 8601), plus optional accumulated_dose, battery, temperature, hardness, serial, firmware. It needs no token and no radwatch on the car. Staleness is computed from ts. The optional fields are absent for about the first minute after a bridge restart; the page shows them as "not yet reported", not as an error.
  • radwatch runs radwatch.py status --db ... on this box. That command opens the database read-only and comes from the companion PR status: read-only JSON for dashboards (CarWatch Radiation page) radwatch#1. CarWatch shows radwatch's numbers and its alarm verdict as radwatch reports them. It does not recompute any statistics.
  • ha reads Home Assistant REST (GET /api/states/sensor.radiacode_*, prefix configurable). The URL and the long-lived token come from radiation.ha.* or $CARWATCH_RADIATION_HA_URL / $CARWATCH_RADIATION_HA_TOKEN. Nothing is hardcoded. The token is sent only to a private, .local or Tailscale host, using the same check mercedesme uses (Security review: CarWatch dashboard (:8088) surface #14). No HA token exists on the fleet today, so this source stays off until one is minted.

States

The page has five states and none of them looks like another: no source configured, source error (with the reason, e.g. "Home Assistant rejected the token (HTTP 401)" or "bridge returned invalid JSON"), connected, no reading yet, stale (older than radiation.stale_seconds, default 60, or age unknown, or a future timestamp from clock skew), and live. If CarWatch itself can't be reached, the page shows that too. radiation.status() never raises: an unexpected exception becomes an error state that carries the exception text.

Files

  • carwatch/radiation.py (new): the three sources and the classifier
  • carwatch/radiation.html (new): the page
  • carwatch/webchat.py: the /api/radiation and /radiation routes, plus a link on the unified page
  • tests/test_radiation.py (new)
  • config.example.json, README.md, FEATURES.md: the config block and docs

Tests

python3 -m unittest tests.test_radiation          # Ran 47, OK (skipped=1)
python3 -m unittest discover -s tests             # Ran 227, OK (skipped=1); main alone: 180 OK
RADWATCH_SCRIPT=/path/to/radwatch.py python3 -m unittest tests.test_radiation.TestRealRadwatch   # 1 OK against radwatch#1

The tests use local HTTP servers and a stand-in radwatch script. Nothing touches the network. The bridge fixture follows the shape of a real bridge reading, with serial and address removed.

Negative control: TestNegativeControl runs the unconfigured / error / stale / live table against two broken versions, one whose classifier drops the age check and one whose status() turns errors into "empty". The table must catch both. I also mutated the source by hand: removing the age check makes 5 tests fail, and swallowing source errors into "empty" makes 16 fail.

Verified vs not

  • Verified: the page rendered in a browser against a local fake bridge in the live, stale, bad-JSON and full-payload states, at desktop and phone width.
  • Not verified: a real Home Assistant, a real RadiaCode, the real bridge, or any device. Nothing was deployed.

🤖 Generated with Claude Code

/radiation shows dose rate, count rate, accumulated dose, battery,
temperature, reading age and radwatch's watch alarm; /api/radiation
serves the same JSON. Served like /nerd (HTML read from disk per
request) behind the same auth.

carwatch/radiation.py reads one of three sources chosen by
radiation.source in config.json:
- json: a bridge's plain-GET JSON object (recommended, no token)
- radwatch: `radwatch.py status` on this box, read-only; the alarm
  verdict is radwatch's own, never recomputed here
- ha: HA REST /api/states/<entity>, token from config or env, only sent
  to a private host (mercedesme._is_private_ha)

Five states that never look alike: unconfigured, error (with reason),
empty, stale (older than stale_seconds, default 60, or age unknown),
live. Fields a source has not sent yet read "not yet reported". status()
never raises; an exception becomes an error state with its text.

tests/test_radiation.py: 47 tests (HA 401/404/unreachable, bridge bad
JSON/missing fields/stale ts, empty radwatch DB, an older radwatch
without status) plus a negative control that collapses the stale and
error distinctions and checks that the scenario table catches it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
carwatch-dev Building Building Preview Oct 7, 2026 2:25pm UTC

Request Review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-07T14:29:39.292889Z 478a9c9 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 478a9c91d6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread carwatch/radiation.py
for key in [k for k, _, _ in FIELDS] + ["last_reading"]:
eid = prefix + key
try:
states[key] = json.loads(_get_json(f"{url}/api/states/{eid}", hdr).decode())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Revalidate redirects before forwarding the HA token

When the private Home Assistant endpoint or its reverse proxy returns a 30x redirect to a public URL, urllib.request.urlopen follows it and copies the Authorization header to the redirected request. Consequently, read_ha can send the long-lived bearer token to a host that never passed _is_private_ha, violating the documented private-host guarantee. Disable redirects or validate every redirect destination and only re-add the header after it passes the private-host check.

Useful? React with 👍 / 👎.

@ThinkOffApp
ThinkOffApp merged commit b2260c4 into main Oct 7, 2026
3 checks passed
ThinkOffApp added a commit that referenced this pull request Oct 9, 2026
HA token requests never follow a redirect (P1 from the #76 review)

This branch was successfully deployed

1 active deployment
Preview — 478a9c91 Deployed Oct 7, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant