About this site

A live conditions dashboard and place guide for Watts Bar Lake, run by Eli Hodapp from a private dock at Tennessee River Mile 559.5.

The mission

Eli at the helm of his pontoon on Watts Bar Lake, mid-channel on a sunny afternoon.
That's me, on the lake, where most of this site gets thought up.

The honest origin story: I got tired of asking "what's the water temperature right now?" in a private bass fishing Facebook group. The question kept coming up, for me and for everyone else who lives on the lake, and the answer was always a guess or a stale screenshot from someone's depth finder a week ago. The data exists. Sensors aren't expensive. There's no good reason a 39,000-acre lake with a thousand families on it should run on group chats and assumptions.

So I threw a probe in the lake and started publishing the number.

And then, because I can't leave anything alone, I thought: this can be better. I started buying stuff to build a real weather station for more live data. Then a camera. Then a forecast pipeline. Then I realized lake elevation, dam generation, marinas, ramps, restaurants, swim spots, and "what's the best boat ramp closest to me?" all had the same problem: useful information scattered across a dozen Facebook groups, official sites that haven't been updated since 2017, and TVA endpoints nobody knows about.

Some of it is genuinely worse than that. A handful of marinas around the lake post their hours and fuel availability not on a Google business listing or a website, but on the personal Facebook profile of whoever runs the place. Not a Page. A profile. Which means to find out whether a marina has gas this Saturday, you have to send the owner a friend request and wait for them to accept it. That's not a joke. That's the actual state of public information on this lake in 2026.

The mission has been the same thing from the first sentence to the most recent commit:

Is this site genuinely useful, and does it make people's days on and around the lake better by giving them easy access to the information they need?

That's the bar every feature has to clear. You shouldn't have to be a member of something to learn things that make a day on the lake better. The whole project runs on that one belief.

watts.bar is free, and it'll stay free. There's no paywall, no email signup, no third-party trackers, no cookies, no cross-site tracking, and no ads, with one exception. The homepage has a single block for my Airbnb on the lake, which I'll cop to. Honestly, I don't care whether anyone books it. It's just one more thing on this lake I've put a lot of effort into and that people seem to enjoy. That's it. No banner network, no affiliate tags on POI pages, no sponsored placements in the directory. Every recommendation on this site is here because I think it deserves to be.

What about analytics?

I run self-hosted Plausible on my own server so I can tell which pages are useful and verify that the site is showing up in search. It's first-party. The script and the data both live on infrastructure I own, not on Google's, not on Cloudflare's, not on anyone else's. No cookies, no cross-site tracking, no personal data collected, and the data is never sold, shared, or used for ads. If you block analytics by default, the site works exactly the same.

The lake microclimate is real

Open your phone and check the weather for Watts Bar. You'll get data from one of two NWS stations: Oak Ridge or Crossville. Oak Ridge is 25 miles northeast in ridge-and-valley terrain. Crossville is up on the Cumberland Plateau, more than 1,200 feet higher than the lake. Neither station experiences the weather we do.

Lakes warm and cool slowly. They moderate the air above them. A sunny day inland can be a fog-on-water morning at the dock. Wind that white-capped Crossville can be glass at TRM 559.5, and the reverse happens just as often. I've watched 10°F differences between the dock and the nearest official station, repeatedly. The forecast model can't see that. It's averaging across a region the lake isn't really part of, climatologically.

The only way to know what the air and water are actually doing on Watts Bar is to put sensors on Watts Bar.

The hardware on the dock

The setup at Tennessee River Mile 559.5 in Roane County, all mounted on the dock or boathouse:

The Sonoff matters more than it sounds like it does. Every other reading on this site has a graceful fallback if my hardware drops out: air temp falls back to NWS, forecast comes from Google, lake level is TVA, alerts are NWS. Water temperature has no public fallback for this stretch of the river. There's no NWS gauge here. So there are two probes, on two different radio protocols, talking to two separate paths into the system. If one fails, the other publishes. If both fail, the page says so instead of pretending.

Where each reading comes from

ReadingSource
Air temperature, humidity, wind, gusts, UV, solar radiation, barometric pressureWS90 on the dock
Rain right now (is it raining this minute)WS90 piezo sensor; instant detection
Rain totals (hourly, daily, monthly)W40BH heated tipping-bucket gauge for accurate accumulation, including winter
Water temperature (primary)WN34BL submerged probe off the dock
Water temperature (backup)SNZB-02LD on the dock; auto-promoted if the WN34BL drops out
Lake elevation, dam outflow, generation statusTVA public APIs (WBOT1 and FLDT1 gauges)
Annual operating curveTVA Watts Bar operating guide
Hourly and 10-day forecastGoogle forecast for TRM 559.5, refreshed every 30 minutes
Radar imageryRainViewer
Map tiles and geographyOpenStreetMap
Active weather alertsNational Weather Service (api.weather.gov)
Air qualityEPA AirNow (nearest reporting monitor is in Athens; I'll buy a sensor for local data soon)
Sunrise, sunset, moon phase, illuminationComputed locally for 35.62°N, 84.71°W
Place details, addresses, hours, ratingsGoogle Places API for over 200 POIs around the lake
POI editorial: vibe, what to know, what people love, tipsHand-written by humans who actually use the lake (or more accurately, a single human right now, me)

How the data flows

The Ecowitt station sends readings every minute to a local Home Assistant instance running on my Proxmox cluster. Home Assistant lives on the same private network as the camera and never exposes itself directly to the public internet. From there, a Cloudflare Worker (live.watts.bar) handles the heavy lifting:

The page reads from R2 first for instant first paint, then upgrades to fresh data from the worker. Your browser never talks to my house.

There's also a Cloudflare Pages middleware running in front of every HTML response. It reads the live cloud-coverage value and stamps data-night plus a sky-color CSS variable onto the <html> element before the page leaves the edge. That's why loading watts.bar at 11pm doesn't flash daytime colors for half a second while JS catches up.

Place data flow

POI data uses a similar pattern. Google Places API calls happen in batches from a build script, never in your browser. Every page on this site is static HTML served from a CDN; the only API calls in the browser are to my Cloudflare worker for live conditions. Google never knows you visited a watts.bar page.

First-party by design

I take privacy seriously, but I also take performance seriously, and the two pull in the same direction here. Every third-party request is a DNS lookup, a TLS handshake, and a chance for someone you didn't sign up to talk to to know you visited. So I went all the way: every font, script, stylesheet, icon set, video player, and map library on this site is self-hosted on the same Cloudflare edge that serves the rest of the page.

In practice:

The four exceptions, which are literally the only external services your browser ever talks to on this site, are listed on the privacy page: OpenStreetMap for map tiles, RainViewer for radar imagery (both impractical to self-host because of how dynamic they are), YouTube embeds on the small number of pages that include video tutorials, and Cloudflare Turnstile on the contact form for spam protection. Every other byte is first-party.

The build script that does the heavy lifting on the live-data side is also worth a mention. Each page render in your browser needs a handful of live signals (current weather, lake elevation, dam generation, cam health, sky color), and these live in different cached files on the server. Rather than fan out four parallel HTTPS calls from the page-rendering middleware to four different worker endpoints, a single /combined.json endpoint bundles all of them into one request. The publisher worker pre-warms the cache once a minute, so the round-trip is constant whether you're the first visitor of the hour or the millionth.

How the live cam works

The camera itself (the G5 Turret Ultra from the hardware list above) is the boring part. The interesting part is what happens after. One viewer or a thousand viewers cost about the same to my home internet.

The G5 has no public IP, no port forward, no VPN exposure. It's a private camera on a private LAN. Your browser never talks to it.

Cold path: idle

When nobody is on the cam page, the camera does almost nothing. A Cloudflare worker pulls a single JPEG snapshot from Home Assistant once every 15 minutes and stuffs it in data.watts.bar (Cloudflare R2 storage). That's enough to keep search engines, AI scrapers, and casual link previews seeing a recent frame. Total upstream during idle: about 50 KB every 15 minutes.

Warm path: someone hits the page

The instant your browser loads /cam/ it requests the cached snapshot from R2, so the page is useful in milliseconds. That same request also pings a small "viewer activity" service running on a dedicated LXC at home (wattsbar-helper on the Proxmox cluster). That service notices someone is watching and starts up a single ffmpeg process that pulls one HEVC video stream from the camera at 9-10 Mbps and transcodes it into three H.264 HLS variants (1080p, 720p, 480p) using the Intel Iris Xe iGPU's hardware encoder. Decode, scale, and encode all happen on the GPU; the CPU sits at about 7%.

The HLS segments and playlists get pushed to R2 with stable filenames, one upload per segment per quality regardless of how many people are watching. Cloudflare's cache distributes those segments globally. Viewers in California, Tennessee, and Tokyo all fetch from their nearest Cloudflare edge, never from my house.

Your browser plays the snapshot for the first few seconds while ffmpeg spins up (the cold start is about 12-15 seconds because the camera takes time to send a fresh keyframe). Once the first HLS segments hit R2, the page transparently swaps the snapshot for live video, picking the quality your bandwidth can sustain. Scroll the cam off-screen or close the tab and the activity service notices the silence; ffmpeg shuts down 60 seconds later and the camera connection closes.

The homepage hero uses one extra trick on top of this. The same shell command on Home Assistant that produces the canonical 1600x900 cam.jpg also produces a downscaled 800x450 variant at /cam-firstload.jpg, around 45 KB. The homepage's first paint loads the small one as the LCP target so cellular visitors see the lake almost immediately, and the JavaScript on the page upgrades to the full-quality cam.jpg (and then to live video) as soon as it can. The canonical full-resolution image is unchanged: that's still what's linked from llms.txt for AI agents, and what gets served to anyone landing on /cam/ directly.

What this means in practice

Why this exists

TVA doesn't operate a public webcam at Watts Bar. The few "Watts Bar lake cam" results that show up elsewhere on the web either point at a hotel pool, a marina parking lot, or haven't refreshed in years. As far as I know, this is the only continuously-updated public view of the actual lake (water, sky, weather, boat traffic) anywhere on the internet. So I built one. The page exists to be glanced at, scrolled past, and occasionally watched.

How the audio filter works

The cam mic captures whatever's audible at the dock: water, wind, birds, boat engines, and sometimes voices when people are nearby. Voices don't get broadcast (see the privacy page for why). The filter that makes that happen is the most over-engineered thing on this site, so here's how it works.

Every cam recording gets sliced into 6-13 second segments by Frigate, the NVR running in a separate LXC on the Proxmox cluster. A daemon on the helper LXC watches that segment directory and processes each segment through the privacy filter the moment it's sealed. Output goes to a separate "cleaned cache" that's the only thing the public broadcast pipelines ever read. If the cleaned cache lags, the live broadcast stalls. If the cleaned cache fails, viewers fall back to a still snapshot. There is no code path where a segment from the raw recording reaches a viewer.

For each segment, the daemon does this:

  1. Builds a seven-segment context window (three segments before, the current segment, three segments after, totaling about 70 seconds). The wider context gives the algorithm room to look around when deciding what to do.
  2. Runs silero-vad across the merged window to detect speech. Threshold tuned aggressive: false positives are cheap, missed speech is expensive.
  3. For every detected speech interval, looks for clean ambient elsewhere in the 70-second window. A passage with no detected speech, at least as long as the speech itself plus crossfade headroom.
  4. If clean ambient is found, the speech is replaced with that ambient via an ffmpeg filter graph that crossfades around the boundaries so the substitution is acoustically smooth.
  5. If no clean ambient is found (long sustained conversation, party at the dock, anything where the wider context is itself filled with speech), the algorithm refuses to guess and the segment goes silent for that interval. Privacy first; UX second.

Both the YouTube live stream and the cam audio on this site read from the cleaned cache. They never touch the raw recordings, so they can't broadcast voices even if a bug introduced a code path that tried. The only configuration knob is the tape delay; the privacy invariant is structural.

The trade-off is the 2.5 minute tape delay everywhere the audio plays. The processor needs the buffer to do the segment merging, VAD inference, clone-source search, and re-encode. For a lake cam, nobody notices.

Note for agents and bulk consumers

This cam is intended for casual human viewing. The architecture above keeps the cost manageable for normal traffic, but the HD video stream is by far the biggest ongoing cost on the site. If you're planning to ingest the cam 24/7 (AI training, automated analysis, public mirroring, or any other always-on use case), please drop me a note first and we'll figure out a sustainable arrangement. The lowest-cost path for ingestion is the snapshot endpoint at https://data.watts.bar/cam.jpg, which gets refreshed every 15 minutes when nobody is watching. Pulling the HD HLS stream around the clock means my home internet is always uploading 6 Mbps for your benefit. Happy to make that work, just ask first.

Other neat things

A few engineering bits I'm fond of and don't see on most lake sites:

Lake-aware distance sorting

When you tap "distance from me" on a directory page, the site doesn't just compute a great-circle haversine number. It loads a simplified polygon of the lake, checks whether the line from your GPS to a place crosses water, and if it does, routes through the nearest of five real bridges. So a place that's four miles as the crow flies but twenty-two miles by road shows the right number. There's even a small modal that asks if you're on the water or on land for GPS coordinates sitting right at the shoreline, since "are you on a boat or in a parking lot" matters and your phone genuinely can't tell.

Forecast accuracy timeline

The site logs every forecast it shows alongside the eventual measured reading. Over months this builds a record of how Google's forecast actually performs for this specific dock, a kind of self-calibrating offset that helps me know whether to trust the high temperature for tomorrow or quietly subtract a few degrees. It's not exposed in the UI yet, but it's running in the background.

The page tints itself to match the sky

The background of every page on this site shifts color throughout the day based on the dominant hue of the live cam frame. Sunrise and sunset actually look like sunrise and sunset on the page. There's no reason it has to do this. I just thought it would be cool, and it is.

Sampling color from the cam at night doesn't work, because CMOS sensors get noisy in low light and the dominant hue ends up being whatever pattern of grain happens to be in the frame. So at night the site does something else: it renders an accurate starfield over the page, showing the stars actually visible from the exact same vantage point as the camera, with the current cloud cover layered on top, as if the camera could see stars at that level of clarity. Why? A better question is why not.

Install it as an app

Watts.Bar is a PWA. On iOS, "Add to Home Screen" gives you the lake icon, the Watts.Bar name, and a launch splash screen with light and dark variants matching what the page renders in the first paint. On Android it installs with three shortcuts: cam, fishing, and the lake-day planner. There's no native binary; the website is the app, which feels like the right amount of effort for something this small.

The build pipeline

Most of this site is static HTML, but there are several hundred pages and they all share editorial conventions, schema markup, breadcrumbs, OG tags, JSON-LD, sitemap entries, and footer layout. A small forest of Python scripts on a Debian LXC handles the publishing: build_directory.py for POI detail pages, build_facets.py for cross-cutting filter pages, build_answers.py for hub-style answer pages, build_tags.py for tag landing pages, build_indexes.py for the directory and guides hubs, build_hubs.py for category hubs, build_sitemap.py, and a few more. They share render_listing.py for cards and maps and render_detail.py for individual POIs.

When I add a feature, it's one place. When I add a new POI, the directory, every relevant hub, the facet pages, the answer pages, the sitemap, and every JSON feed all update from one source on the next build. The whole site rebuilds end to end in seconds.

I might actually publish all of this at some point. I love complex systems engineering, and this is one hell of a complex system that other lunatics might appreciate.

For LLMs and AI agents

This site is fully agent-ready, and I treat that as a feature, not an afterthought. If you're building an assistant that needs to know things about Watts Bar Lake, you don't have to scrape HTML.

The site is listed at isitagentready.com, which is a good general checklist for making your own site accessible to agents. watts.bar is green across the board there except for the OAuth-related items, because watts.bar doesn't use any authentication: the site is totally free and there's nothing to log in to. The short version: structured data, machine-readable feeds, and a /llms.txt go a very long way.

Licenses and reuse

Original editorial and code on this site are released under Creative Commons Attribution 4.0. Use it, remix it, build something with it. Just credit "watts.bar" with a link.

Live data, place details, photos, and map tiles from third parties (TVA, NWS, Google, OpenStreetMap, RainViewer, EPA AirNow) are property of their respective owners and aren't mine to relicense. The Google Places photos in particular are subject to Google's terms.

JSON feeds you can pull directly:

If you build something with these, I'd love to hear about it.

Privacy

watts.bar runs traffic analytics through a self-hosted Plausible instance (no cookies, no cross-site tracking, no ad networks). Page views, referrer, country, and device class are recorded; no IP retention beyond the rate-limiting window.

If a future iOS or Android app of mine asks you to opt in to push notifications, here's what gets stored: your device's push token, an installation UUID generated by the app on first launch, your selected notification topics, your timezone, and the app version. Storage is a private Cloudflare R2 bucket pinned to North American (ENAM) data centers. Cloudflare is the only sub-processor. Tokens are never sold or shared. You can revoke at any time by toggling topics off in the app, requesting full deletion via an in-app "Forget my data" button, or uninstalling. Note that uninstall does not delete the stored data automatically; the cleanup process prunes inactive entries after 90 days. When you register for push, your IP address is briefly visible to Cloudflare for rate-limiting; it is not retained.

Contact

Spotted something wrong, want to add a place, or have a question? The contact form sends straight to me. Or find all the other dumb crap I'm up to at hod.app.

Frequently asked questions

Who runs this site?

Eli Hodapp, from a private dock at Tennessee River Mile 559.5 on Watts Bar Lake in Roane County, Tennessee. Personal project, no service-level agreement, no business model. Built because something like this should exist.

How accurate are the weather readings?

The readings come from sensors on the dock itself, not from a regional airport or an inland NWS station. That makes them substantially more accurate for on-lake conditions than any standard weather source. Air readings update every minute. Water temperature uses two redundant probes; if either drops out, the other takes over automatically.

What happens if your hardware goes down?

Most readings have a graceful fallback. Air temp falls back to the nearest NWS station. Forecast comes from Google. Lake elevation and dam generation come from TVA. Alerts come from NWS. Even the dock cam frame is cached, so the worst case there is that you're looking at a slightly stale photo instead of one from a few seconds ago. Water temperature has two on-dock probes; if both fail, the page says so rather than pretending.

Can I use the data or text from this site?

Yes. Original editorial and code are released under Creative Commons Attribution 4.0. Credit "watts.bar" with a link. Machine-readable feeds are listed in /llms.txt. Third-party data and photos remain with their original owners.

How do I report an error or add a listing?

Use the contact form. Include the page URL and what's wrong or missing. Marinas, events, ramps, parks, restaurants, hours updates, anything: all welcome.