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
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:
- Ecowitt GW3000B base station and gateway.
- Ecowitt WS90 7-in-1 array: temperature, humidity, wind speed and direction, gusts, UV, solar radiation, piezo rain detection, barometric pressure.
- Ecowitt W40BH heated tipping-bucket rain gauge for accurate winter rainfall totals.
- Ecowitt WN34BL waterproof probe submerged off the dock; primary water-temperature reading.
- Sonoff SNZB-02LD Zigbee temperature sensor mounted on the dock; backup water-temperature reading.
- Ubiquiti G5 Turret Ultra mounted on a dock piling, pointed at the main channel.
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
| Reading | Source |
|---|---|
| Air temperature, humidity, wind, gusts, UV, solar radiation, barometric pressure | WS90 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 status | TVA public APIs (WBOT1 and FLDT1 gauges) |
| Annual operating curve | TVA Watts Bar operating guide |
| Hourly and 10-day forecast | Google forecast for TRM 559.5, refreshed every 30 minutes |
| Radar imagery | RainViewer |
| Map tiles and geography | OpenStreetMap |
| Active weather alerts | National Weather Service (api.weather.gov) |
| Air quality | EPA AirNow (nearest reporting monitor is in Athens; I'll buy a sensor for local data soon) |
| Sunrise, sunset, moon phase, illumination | Computed locally for 35.62°N, 84.71°W |
| Place details, addresses, hours, ratings | Google Places API for over 200 POIs around the lake |
| POI editorial: vibe, what to know, what people love, tips | Hand-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:
- Pulls fresh weather from Home Assistant over a Cloudflare tunnel.
- Hits TVA's APIs for lake elevation and generation.
- Hits Google's forecast API every 30 minutes.
- Hits the National Weather Service for active alerts and watches.
- Hits AirNow for air quality from the nearest reporting monitor.
- Hits RainViewer for radar tiles.
- Caches everything into a Cloudflare R2 bucket (
data.watts.bar).
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:
- No requests to fonts.googleapis.com or fonts.gstatic.com. The four type families this site uses (DM Serif Text for headlines, Cormorant Garamond for accents, Montserrat for UI, Oswald for emphasis) are pulled in at build time and live alongside the rest of the site at
/fonts/. - No requests to cdn.jsdelivr.net or unpkg.com. The HLS video player, the Leaflet map library, the marker clustering plugin, and the weather-icon font are all pinned to specific versions and vendored under
/vendor/. There's a smallfetch_vendor.pyscript that refreshes them when upstream releases a security patch. - No third-party analytics. Plausible runs on my own hardware, tunneled through my own VPN.
- No CDN-fetched anything that ships to your browser. If it's a request your browser makes, it's a request to watts.bar (or one of its sibling subdomains for live data and image storage).
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
- One viewer watching: my dock uploads about 6 Mbps to Cloudflare.
- 1,000 viewers watching: my dock still uploads about 6 Mbps to Cloudflare. Cloudflare handles the fan-out.
- Nobody watching: my dock uploads about 50 KB every 15 minutes.
- Bad connection viewer: the page detects low bandwidth and stays on the cheap snapshot view rather than choking on HD video.
- Server hiccup or camera offline: a status check tells the page to stay on snapshot mode and show a polite banner instead of trying to load video that isn't coming.
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:
- 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.
- Runs silero-vad across the merged window to detect speech. Threshold tuned aggressive: false positives are cheap, missed speech is expensive.
- 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.
- 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.
- 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.
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.
- /llms.txt: a plain-text index of every machine-readable resource, written for LLMs to read directly.
- The boring basics:
/sitemap.xmllists all 322 URLs and/robots.txtdeclaresai-train=yes, search=yes, ai-input=yesvia the Content-Signal header so crawlers don't have to guess. - Schema.org JSON-LD on every page: LocalBusiness and FoodEstablishment subtypes, FAQPage, Speakable, CollectionPage, ItemList, BreadcrumbList. Real structured data, not just lip service.
- MCP server card at
/.well-known/mcp/server-card.json: declares the site as a Model Context Protocol resource so MCP-aware clients can introspect it. - Agent skills at
/.well-known/agent-skills/index.json: per-task SKILL.md entries describing how to use every live and static feed. - WebMCP integration for connected agents to query the site's data without round-tripping through HTML.
- Free public-domain CC0 lake vector files for every reservoir in the Tennessee River system (Watts Bar, Fort Loudoun, Tellico, Chickamauga, Norris, Cherokee, Douglas, and the rest, 27 lakes total). Directory at
/free-lake-vectors/; per-lake URL pattern/lake-vector/<slug>/; mega-bundle on R2. Agent-skill atget-lake-vector.md. SVG, PDF, EPS, DXF, GeoJSON, KML, Shapefile, plus high-res PNG. No attribution required.
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:
- Live conditions JSON: air, water, wind, every sensor reading, refreshed every minute.
- TVA dam generation JSON: Watts Bar and Fort Loudoun, real-time.
- POIs database JSON: full directory with editorial fields, structured amenities, controlled-vocab tags, region, lakeside class, Google Places metadata. Schema version 1.
- Marinas JSON
- Boat ramps JSON
- Parks and campgrounds JSON
- Dining JSON
- Fishing knowledge base JSON
- Lake regions JSON
- Events feed JSON
- Bridges (GeoJSON): used for the lake-aware distance routing above.
- Lake polygon, simplified (GeoJSON): ~950 vertices with a small inward buffer; what the site actually uses.
- Lake polygon, full resolution (GeoJSON): ~2.4 MB. Probably useless for most things due to its complexity, but it's here if you need it.
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.