Bring real-time U.S. water data into Home Assistant. This integration polls the USGS National Water Information System (NWIS) for any active monitoring location in the United States — streams, lakes and reservoirs, canals, tidal gages, groundwater wells, and continuous water-quality sites — and exposes each reported measurement as a sensor, with automatic handling of seasonal shutdowns and built-in rate-of-change/trend sensors for level and flow.
New installs use the USGS Water Data OGC API (the modern replacement for the retiring WaterServices API). A free api.data.gov key is recommended — a shared demo key covers light use. Existing sites on the legacy backend keep working but are deprecated — see Data sources.
One gauge → one device, with a sensor per reported parameter. Here the
tidal Willamette River at Portland shows discharge, tidally-filtered discharge,
gauge height, water-quality readings, water velocity, and the built-in rate &
trend sensors — with a live trend feed on the right. (Instantaneous discharge
goes negative on the incoming tide, which is exactly why the tidally-filtered
value is also reported.)
- Features
- Use cases
- Known limitations
- Requirements
- Installation
- Setup
- Configuration (Options)
- Sensors
- Data sources
- Seasonal & offline handling
- Automation examples
- Troubleshooting
- How it works
- Contributing
- Search by name or site number — find any active USGS monitoring location from the setup wizard, across all site types (not just streams).
- Up to 21 measurement sensors per site, created only where the site
actually reports them:
- Flow & level — discharge, gauge height, tidally-filtered discharge, water velocity, depth to water level, reservoir elevation, reservoir storage
- Water quality — water temperature, specific conductance, dissolved oxygen and % saturation, pH, turbidity, salinity, nitrate, chlorophyll
- Weather — air temperature, relative humidity, wind speed, wind direction, precipitation
- Built-in rate-of-change and trend sensors for level/flow parameters (e.g. gauge-height rise in ft/hr and a Rising / Falling / Steady trend).
- Opt-in percent-of-normal sensors — Condition, Percentile, and % of Normal for discharge, depth-to-water, and gauge height vs. ~30 years of daily history (the USGS WaterWatch / Groundwater Watch view), with depth-to-water correctly inverted (gauge height gets Condition + Percentile only).
- Station Status sensor —
ActiveorOffline, with a reason, so seasonal and winter shutdowns are handled cleanly. - No phantom entities — a sensor is created only for a parameter the site genuinely serves.
- Per-site configuration — poll interval and which parameters become sensors.
- Multiple sites — add as many as you like; each becomes its own device.
- Runs on the new USGS Water Data OGC API by default — the modern replacement for the retiring WaterServices API. Existing legacy sites keep working (now deprecated); see Data sources.
- Flood / high-water watch — automate on Gauge Height or its Rate/Trend (e.g. notify when a river rises faster than X ft/hr, or crosses a level).
- Drought & low-flow awareness — use Discharge Condition / % of Normal to flag when a stream is running below normal for the season.
- Recreation — trigger on discharge/level bands for paddling, fishing, or swimming-hole conditions.
- Groundwater / well monitoring — track Depth to Water Level and its (inverted) Condition to watch an aquifer trend down or recover.
- Dashboards — chart long-term trends since most sensors carry the
measurementstate class and work with HA's long-term statistics.
- Percent-of-normal needs history & a real zero. Stats sensors require about
5 years of daily record for a gauge (very new gauges stay
unavailable). Gauge height gets Condition + Percentile only — its arbitrary datum makes a "% of normal" meaningless — and USGS publishes a daily-mean stage record for only a minority of gauges, so gauge-height stats often won't appear. - Tidal gauges are noisy for stats. Comparing an instantaneous reading to a daily-mean envelope swings a lot on tidally-influenced sites (faithful to WaterWatch, just jumpy).
- Provisional data. Recent USGS values are provisional (not quality-assured)
and may be revised; the Modern backend exposes an
approval_statusattribute and a standardizedprovisionalboolean for automations. - Transmission cadence. USGS often transmits about hourly even though it
records every ~15 minutes, so the "latest" value can repeat across polls
(
last_reading_timeshows the true observation time). - Modern backend is the default and needs a key. It targets USGS's Water
Data OGC API (which USGS still versions as
v0and will move to a new URL atv1— the integration isolates that so upgrades are seamless) and works best with a free api.data.gov key. The Legacy backend is deprecated and slated for removal.
- Home Assistant 2026.3 or newer
- HACS (recommended) or manual installation
- A free api.data.gov key is recommended for the default Modern backend (a shared demo key works for light use). No key is needed to search for and add a gauge.
USGS Streamflow is in the default HACS store:
- Open HACS in Home Assistant.
- Search for USGS Streamflow.
- Click Download.
- Restart Home Assistant.
Not showing up yet? Add it as a custom repository
If the default-store listing hasn't reached your instance yet, you can add the repo directly:
- Open HACS → three-dot menu → Custom repositories.
- Add
https://github.com/yieldhog/usgs_streamflowas an Integration. - Find USGS Streamflow, click Download, then restart Home Assistant.
To try a pre-release: open USGS Streamflow in HACS → three-dot menu → Redownload → enable "Show beta versions" (or "Need a different version?") → pick the beta, then restart Home Assistant. Pre-releases are hidden from the normal update flow, so you opt in explicitly.
- Download the latest release from the releases page.
- Copy the
custom_components/usgs_streamflowfolder into your HAconfig/custom_components/directory. - Restart Home Assistant.
This integration follows standard Home Assistant removal — no extra cleanup.
- Go to Settings → Devices & Services, open the USGS Streamflow card.
- For each gauge entry, use its ⋮ menu → Delete. This removes the entry, its device, and all of its sensors, and clears the on-disk percent-of-normal cache for that gauge.
- (Optional) To uninstall the integration entirely, remove USGS Streamflow from HACS, then restart Home Assistant.
- Settings → Devices & Services → Add Integration.
- Search for USGS Streamflow.
- Find your gauge:
- By name — enter part of a station name (e.g.
Bear Creek) and a two-letter state code (e.g.CO). A state is required for name searches. - By site number — paste a USGS site number directly (e.g.
06711565); no state code needed. AUSGS-prefix is accepted and stripped. - Not sure of the number? Browse the
USGS National Water Dashboard or a
site's page at
https://waterdata.usgs.gov/monitoring-location/<site_id>/to see exactly which parameters it reports.
- By name — enter part of a station name (e.g.
- Pick your site from the results list. (Optionally set an API key here — see Data sources. You can also add it later in Options.)
- Repeat to add more sites.
Note: Site search always uses the USGS WaterServices Site Service (the legacy backend), so no API key is needed to find and add a gauge — that's why a two-letter state code is required for name searches. The Data source option only affects how the gauge polls afterwards (Legacy WaterServices or the Modern OGC API).
Search by name + state (or paste a site number) → pick the station and optionally add an api.data.gov key → it's added as a device.
Each site becomes its own device; its sensors are grouped under it.
After adding a site, click Configure on the entry to adjust:
| Option | Description |
|---|---|
| Update interval | How often to poll USGS, in minutes (minimum 15). USGS instantaneous data refreshes about every 15 minutes, so polling faster adds load without new data. |
| Parameters to show | Uncheck measurements you don't want as sensors. Unchecked parameters won't create sensors even if the site reports them. |
| API key (recommended) | An api.data.gov key for the Modern backend (the default). Leave blank to fall back to a shared, rate-limited demo key. Ignored by legacy entries. Pre-filled from another entry if you've already set one. |
| Percent-of-normal sensors (opt-in) | Adds Condition, Percentile, and % of Normal sensors for discharge, depth-to-water, and gauge height (gauge height: Condition + Percentile only) by comparing the live reading to ~30 years of daily history. Off by default — the first build downloads the long-term record per gauge, then caches it on disk. See Condition & percent-of-normal sensors. |
| Data source (advanced) | Legacy (WaterServices — stable default) or Modern (Water Data OGC API). Only shown when Home Assistant Advanced Mode is enabled in your user profile. |
Per-site options — pick which parameters become sensors, and (with Advanced
Mode) switch the Data source between Legacy and Modern.
Changing any option reloads that entry.
Each configured site creates one device. Under it you'll get a Station Status sensor plus a measurement sensor for every enabled parameter the site reports, and rate/trend sensors for level/flow parameters.
| Entity | Unit | USGS code | Notes |
|---|---|---|---|
| Discharge | ft³/s | 00060 | Volumetric flow (CFS) |
| Gauge Height | ft | 00065 | Water level above the gauge datum |
| Discharge (Tidally Filtered) | ft³/s | 72137 | Tidally filtered flow |
| Water Velocity | ft/s | 72255 | Mean velocity |
| Depth to Water Level | ft | 72019 | Below land surface (groundwater wells) |
| Reservoir Elevation | ft | 62614 | Water-surface elevation; also read from 00062 (above datum) and 62615 (NAVD 1988), normalized to one sensor |
| Reservoir Storage | acre-ft | 00054 | |
| Water Temperature | °C | 00010 | |
| Specific Conductance | µS/cm | 00095 | At 25 °C |
| Dissolved Oxygen | mg/L | 00300 | |
| Dissolved Oxygen (% Saturation) | % | 00301 | |
| pH | — | 00400 | Standard units |
| Turbidity | FNU | 63680 | |
| Salinity | ppt | 00480 | Parts per thousand |
| Nitrate | mg/L | 99133 | As nitrogen (nitrate + nitrite) |
| Chlorophyll | RFU | 32316 | Relative fluorescence |
| Air Temperature | °C | 00020 | |
| Relative Humidity | % | 00052 | |
| Wind Speed | mph | 00035 | |
| Wind Direction | ° | 00036 | Degrees clockwise from north |
| Precipitation | in | 00045 | Incremental per reporting interval, not an accumulating total |
Sensors are created only for parameters a site actually reports — you won't
get empty entities for measurements a site doesn't have. Most measurement
sensors are stored with the measurement state class so they work with the
Statistics/long-term-stats features.
For the level/flow parameters where rate-of-rise is the canonical signal (Gauge Height, Discharge, Depth to Water Level), the integration also creates, automatically:
| Entity | Type | Notes |
|---|---|---|
| … Rate | ft/hr or ft³/s/hr | Per-hour rate of change over a trailing 60-minute window of real observations |
| … Trend | enum | rising / falling / steady, with a small dead-band so noise reads as steady |
These are computed in-memory from a trailing buffer of real observations. On
startup the buffer is warm-started with the last ~3 hours of data (one small
fetch per parameter), so Rate/Trend report immediately after a restart or
reload instead of waiting to accumulate. If that warm-up fetch is unavailable
they fall back to filling over the first couple of polls, reporting unknown
until there are at least two distinct readings in the 60-minute window.
Opt-in (enable Percent-of-normal sensors in the options). For Discharge
(00060), Depth to Water Level (72019), and Gauge Height (00065),
the integration builds a day-of-year envelope from ~30 years of USGS
daily-mean values and places the live reading against it — the same view as
USGS WaterWatch (streamflow) and Groundwater Watch (water levels):
| Entity | Unit | Notes |
|---|---|---|
| … Condition | enum | Much below normal / Below normal / Normal / Above normal / Much above normal (percentile bands <10 / 10–25 / 25–75 / 75–90 / >90) |
| … Percentile | % | Where today's reading falls in its calendar day's historical range |
| … % of Normal | % | Reading as a percentage of the day's historical median (unavailable on days whose historical median is zero, where the ratio is undefined) |
Depth-to-water is inverted throughout: a deeper reading means less
groundwater, so a deep level reads as below normal (Condition), a low
percentile, and below 100% of normal — the % of Normal is flipped to
median ÷ depth so it tracks water rather than depth and stays consistent with
the other two. This matches Groundwater Watch.
Gauge height gets Condition and Percentile only — no % of Normal. It's measured from an arbitrary local datum, so comparing the gauge to its own history (percentile/condition) is meaningful, but a ratio to the median isn't (discharge and depth-to-water have real zeros; gauge-height datum does not). Note that USGS publishes a daily-mean stage record for only a minority of gauges, so the gauge-height stat sensors are created only where that record exists — on most gauges you'll see them for discharge (and depth-to-water) but not gauge height.
The long-term record is fetched once per gauge (a heavy pull), then persisted
on disk and refreshed only about monthly, so it costs nothing on normal polls.
Condition/Percentile/% of Normal then update with each live reading. The 30-year
request is a ceiling, not a requirement — a gauge just uses whatever history it
has. A calendar day needs about 5 years of values before it reports, so very
new gauges (under ~5 years) stay unavailable, but a typical multi-year record
is plenty.
Always present. State is Active or Offline; when offline it carries an
offline_reason (seasonal/stale). See
Seasonal & offline handling.
| Entity | Attributes |
|---|---|
| Measurement sensors | usgs_site_id, last_reading_time (when available). On the Modern backend also approval_status (Approved/Provisional), a provisional boolean (true until USGS approves the value), qualifier, statistic_id, time_series_id. |
| Rate / Trend sensors | usgs_site_id, window_minutes, sample_count (Trend also rate_per_hour). |
| Condition / Percentile / % of Normal | usgs_site_id, percentile, percent_of_normal, condition, median, sample_count, observation_date, inverted, plus record_years / record_start / record_end. |
| Station Status | usgs_site_id, usgs_waterdata_url, and offline_reason when offline. |
USGS is retiring the legacy WaterServices API (decommission planned for Q1 2027) in favor of the modern Water Data OGC API. This integration supports both, selectable per site:
| Modern (default) | Legacy (deprecated) | |
|---|---|---|
| API | Water Data OGC API (api.waterdata.usgs.gov) |
NWIS WaterServices (waterservices.usgs.gov) |
| API key | Recommended (free; a built-in demo key works for light use) | Not used |
| Status | Default for new installs | Being retired by USGS (~Q1 2027); a future release removes it |
| Extra data | Approval status (+ a provisional flag), qualifier, and statistic/series-id attributes |
— |
New installs use the Modern backend and search for gauges through it — a free api.data.gov key is recommended (the shared demo key covers light use). Existing sites keep polling whatever they were set to: legacy entries are untouched, but show a deprecation notice — switch them to Modern (Advanced Mode → Configure → Data source, and add a key) before USGS retires the legacy API.
The modern API is gated by api.data.gov, a free, shared key service used across many U.S. government APIs.
- Go to https://api.data.gov/signup/.
- Enter your name and email; the key is emailed to you immediately (no approval wait).
- In Home Assistant, open the site's Configure dialog and paste the key into API key. It's reused as the pre-filled default for new sites.
About the demo key: if you leave the field blank while using the Modern
backend, the integration falls back to the shared DEMO_KEY. That key uses
api.data.gov's small shared demo allowance (on the order of a few dozen requests
per hour) — fine to try one site, but it will hit rate limits (HTTP 429) with
several sites or frequent polling. A free personal key raises the limit
substantially. The integration backs off automatically on 429 responses.
Existing legacy entries keep working but are deprecated. To move one:
- Enable Advanced Mode in your Home Assistant user profile (so the Data source option appears).
- Get a free api.data.gov key (or rely on the demo key for light use).
- Open the site's Configure dialog, set Data source → Modern, and add the key. The entry reloads on the new backend; the deprecation notice clears.
Please
report any value,
parameter, or offline-detection mismatches you notice after switching. See
MIGRATION.md for the full migration plan and status.
Many gauges are seasonal (e.g. ice-affected or summer-dry) or temporarily stop transmitting. The integration handles this explicitly:
- The Station Status sensor reports
Offlineand explains why viaoffline_reason(e.g. "data is stale — last reading …" or "seasonal or discontinued"). - Measurement sensors for that site become
Unavailable(not a misleading stale value orUnknown) while it's offline, and recover automatically when the gauge resumes. - A parameter a site has never reported is treated as "not present" — no entity is created for it.
The examples/usgs_streamflow_examples.yaml
package shows how to build on these sensors using only Home Assistant's built-in
helpers (statistics, derivative, trend, threshold, template, automation):
- Rolling statistics — 24-hour average discharge, 7-day max gauge height, net change
- Flow Condition and a Good To Fish flag based on your own thresholds
- Alerts — rapid rise, action-stage crossing, gauge offline, and high water temperature
Replace the placeholder entity ids with your own and tune the thresholds — flow ranges and flood/action stages are specific to your water body and (for stage) the gauge's datum.
- No sensors were created for my site. The integration only creates sensors
for parameters the site currently reports. Open
https://waterdata.usgs.gov/monitoring-location/<site_id>/and check "Available parameters." Seasonal gauges may report nothing out of season. - A measurement shows
Unavailable. The site is offline/seasonal, or it doesn't serve that parameter. Check the Station Status sensor'soffline_reason. - Rate/Trend sensors show
unknown. On restart/reload they're warm-started from recent history and should populate on the first poll. If they linger onunknown, the warm-up fetch was unavailable (or the river genuinely hasn't moved — a flat river readssteady, notunknown); they'll fill once there are two distinct readings in the 60-minute window. HTTP 429/ rate-limit errors (Modern backend). You're using the demo key or polling many sites; add a free personal api.data.gov key and/or raise the update interval.- Values look unchanged for a while. USGS typically transmits about hourly
(more often during floods) even though it records every ~15 minutes, so the
published "latest" value can be the same across several polls. The per-sensor
last_reading_timeattribute shows the true observation time. - Filing an issue? Open the integration's ⋮ → Download diagnostics — the redacted dump (the API key is never included) shows the backend in use, the parameters the site reports, offline reason, and stats envelope metadata, which makes "wrong status" / "no data" / "missing stats" reports much faster to diagnose.
- Polls the selected USGS API per site on your configured interval (default 15 min) via a single Data Update Coordinator.
- All USGS access goes through one swappable client, so Legacy and Modern backends are interchangeable and the rest of the integration is backend-agnostic.
- With the percent-of-normal feature enabled, a second coordinator builds each gauge's day-of-year percentile envelope from ~30 years of daily-mean values, persists it to disk, and refreshes it only about monthly — so the live sensors just compare each reading to the cached envelope with no extra polling cost. That first envelope build is time-bounded, so a slow USGS response never stalls setup; the measurement sensors load promptly and the envelope finishes in the background.
- Two test suites run in CI: a dependency-free unit suite (
tests/, run withpython -m unittest discover -s tests -t .) covering parsing, offline detection, the rate/trend buffer, and the percent-of-normal logic; and a Home Assistant harness suite (tests_ha/, run withpip install -r requirements_test.txt && pytest) covering the config/reauth/ options flows and the setup/unload lifecycle.
Issues and pull requests welcome at github.com/yieldhog/usgs_streamflow. Data courtesy of the U.S. Geological Survey; this project is not affiliated with or endorsed by the USGS.


