Aller au contenu

omnilab/tides

Omnilab Tides: NOAA CO-OPS (Tides and Currents) - a station's water level every six minutes, verified hourly heights and highs and lows, tide predictions (every six minutes, hourly, or the highs and lows), water and air temperature as series, at a station named or the nearest one of a point, cut to the API's length limits; a station as an entity; through the CO-OPS Data and Metadata APIs, no key needed.

Étage
Fournisseur · Omnilab
Version
1.x
Suite de conformité
non branchée
Vérifié en réel
Vérifié en partie
Licence
MIT
composer require omnilab/tides:1.x-dev

NOAA CO-OPS (Tides and Currents), through its Data API and Metadata API (https://api.tidesandcurrents.noaa.gov/), for omnisense/omnilab: a station's water level every six minutes, verified hourly heights and highs and lows, tide predictions, water and air temperature, at a coops.station or the nearest station of a point, as series; the station itself as an entity. No key.

Installation

composer require omnilab/tides
use Omnilab\Tides\TidesSourceFactory;

$tides = (new TidesSourceFactory($httpClient))->create([
    'mailto' => 'support@glitchr.io',  // optional
    'datum' => 'MLLW',                 // optional: the water levels' datum
    'radius_km' => 50,                 // optional: how far from a Point the nearest station is looked for
    'throttle' => 0.5,                 // optional: seconds between two calls
]);

With the Symfony bundle, the factory is registered as tides when the bundle knows it; otherwise register TidesSourceFactory as a service.

Credentials

None: CO-OPS's Data and Metadata APIs take no key and no account. The API logs the calls under the application name, and asks its callers to space their calls.

Options

The factory's options, with the defaults it applies (TidesSourceFactory):

Option Type Default Required Effect
datum string MLLW no the water levels' datum, upper-cased: MLLW (the charts'), MSL, MHHW, MLW, NAVD, STND...
radius_km float 50 no how far from a Point the nearest water level station is looked for, in kilometres
application string omnilab no the name the API logs the calls under, sent as application
metadata_uri string https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi/ no the Metadata API, for a station (stations/{id}.json) and the list the nearest one is chosen from
mailto ?string null no a contact address, sent in the User-Agent
user_agent ?string null no the whole User-Agent, in place of the one made from mailto
throttle float 0.5 no seconds left between two calls: the API asks its callers to space them
base_uri string https://api.tidesandcurrents.noaa.gov/api/prod/datagetter no the Data API (datagetter)

Capabilities

capabilities() answers entity, series, forecast, historical, station; an operation it does not answer throws NotSupportedException before any call.

Capability Supported
entity yes a CO-OPS station (coops:8518750, coops.station:, its page, its Metadata API record); another scheme refused before any call; one call
search no
series yes the variables of Calls at a coops.station or at the nearest water level station of a Point within radius_km; one call per variable and per slice of the API's length limit; no station near: a series without points per variable, at the point; an unknown station: an empty list
products no
occurrences no
datasets no
jobs no
open no
structure no
cross_references no none: the SHEF code (the NWS's) is a property
forecast yes the astronomical predictions (forecast true, past and future alike)
historical yes the observed and verified water levels, the temperatures
area no a box is refused before any call
station yes coops.station
cloud_cover no

Which API

CO-OPS's Data API (https://api.tidesandcurrents.noaa.gov/api/prod/datagetter): one call per product and period, units=metric, time_zone=gmt, format=json, application=omnilab, the datum for the water levels; and its Metadata API (https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi/): stations/{id}.json for a station, stations.json?type=waterlevels (302 stations on 2026-10-15, read once per source) for the nearest one of a point. Errors come as {"error": {"message": ...}} with a 200 (a 400 for some); "No data was found" (a product the station does not have, verified data not yet published) is a series without values, any other message a ProviderException.

Its shapes

A station is an entity of the core's coops.station scheme, and its records are series (Capability::STATION); a Point answers its nearest water level station within radius_km; a BBox is refused. The predictions are series with forecast true (astronomical predictions, for the past as for the future).

Calls

Method Reads Calls
entity() coops:8518750, coops.station:8518750, its page on tidesandcurrents.noaa.gov, its record in the Metadata API stations/{id}.json
series() the variables below; a coops.station or a Point; the period (today by default; a date alone is the whole day, in the query's time zone, UTC by default); a step and an aggregation one call per variable and per slice of the API's length limit, half a second apart
Variable Product (interval) Unit Per request
water_level water_level: observed, six minutes, preliminary or verified m above the datum 31 days
hourly_height hourly_height: verified, hourly m 365 days
high_low high_low: verified highs and lows (irregular) m 365 days
predictions predictions (interval=6) m 31 days
hourly_predictions predictions (interval=h) m 365 days
high_low_predictions predictions (interval=hilo, irregular) m 365 days
water_temperature, air_temperature six minutes °C 31 days

The regular series are put on the step's grid from the start of the period: without an aggregation the value at each step's start (null when none), with one (Series::MAX, MIN, MEAN, SUM) the step's aggregate; PT6M gives every six-minute value. The highs and lows come as they are (step null). A period longer than a product's limit is asked in slices (75 days of six-minute water levels: three calls). Another variable or aggregation, a box or another scheme's station are refused before any call. An unknown station is null (404).

What an entity holds

id (coops.station:8518750), label (The Battery), domain earth, kind station, place, url (its station page), and state, time_zone (EST (UTC-5)), tide_type (Mixed), tidal, great_lakes, shef_code (the NWS's code: BATN6), affiliations.

The Collector

No path: the APIs give no id of another base. A CO-OPS station's SHEF code is the NWS's, kept as a property, not an identifier of the nws.station scheme (whose stations are observation ones).

What the APIs do not give here

The currents and their predictions, wind, air pressure, conductivity, salinity, visibility, humidity (units the API leaves to "appropriate for the data"), one-minute water levels, daily and monthly means, the datums, harmonic constituents, flood levels, benchmarks, the OFS model forecasts, the high tide flooding outlooks, and the derived products API are not read.

Example

<?php

require __DIR__.'/vendor/autoload.php';

use Omnilab\Model\Point;
use Omnilab\Source\SeriesQuery;
use Omnilab\Tides\TidesSourceFactory;
use Symfony\Component\HttpClient\HttpClient;

$tides = (new TidesSourceFactory(HttpClient::create()))->create(['mailto' => 'you@example.org']);

$battery = $tides->entity('coops:8518750');
echo $battery->label, ' (', $battery->id, '), ', $battery->value('state'), ', ', $battery->value('tide_type'), ' tide, SHEF ', $battery->value('shef_code'), "\n";

$day = new DateTimeImmutable('2026-10-26');
[$level, $highLow] = $tides->series(new SeriesQuery(['water_level', 'high_low_predictions'], $battery->id, $day, $day));
printf("%s: %d water levels (%s to %s %s)\n", $level->label, count(array_filter($level->values(), 'is_numeric')), $level->min(), $level->max(), $level->unit);
foreach ($highLow as [$time, $value]) {
    echo '  predicted ', $time->format('H:i'), ' UTC ', $value, " m\n";
}

[$near] = $tides->series(new SeriesQuery(['hourly_predictions'], new Point(40.70, -74.02), new DateTimeImmutable('2026-10-28'), new DateTimeImmutable('2026-10-28')));
printf("nearest station of 40.70,-74.02: %s, %d hourly predictions, max %s m\n", $near->label, count($near), $near->max());
The Battery (coops.station:8518750), NY, Mixed tide, SHEF BATN6
The Battery (8518750), MLLW: 24 water levels (-0.043 to 1.79 m)
  predicted 00:13 UTC 1.563 m
  predicted 06:33 UTC -0.033 m
  predicted 12:27 UTC 1.823 m
  predicted 19:19 UTC -0.054 m
nearest station of 40.70,-74.02: The Battery (8518750), MLLW, 24 hourly predictions, max 1.837 m

(as answered on 2026-10-27, half a second between calls; the water level is the value at each hour's start: SeriesQuery::HOURLY by default)

# config/packages/omnilab.yaml
omnilab:
    sources:
        tides: { factory: tides, options: { mailto: '%env(OMNILAB_MAILTO)%', datum: MLLW, radius_km: 50 } }

Errors

Every call goes through HttpSource: a 404 or a 410 (and a 204) is null - an empty page for a list -; a 429 is a RateLimitedException (retryAfter from Retry-After); a 5xx, a time-out or a host that does not answer an UnavailableException; any other 4xx a ProviderException with its status and what the service said; an answer that is not JSON a ProviderException, a web page answered for JSON an UnavailableException.

CO-OPS's own: its errors come as {"error": {"message": ...}}, often with a 200: "No data was found" (a product the station lacks, verified data not yet published) is a series without values, any other message a ProviderException with it ("Range Limit Exceeded" never: the source cuts the period into slices). Before any call (NotSupportedException): a variable not served, an aggregation it does not make, a box, a station of another scheme. An unknown station is the Metadata API's 404, null.

Verified, and not

Against the real APIs on 2026-10-15: The Battery by its id (New York, a mixed tide) and an unknown station (404, null); its water level, six-minute predictions, predicted highs and lows (1.363, 0.228, 1.581, 0.208 m) and water temperature (18.4 to 18.8 °C) on 2026-10-14, hourly and every six minutes (240 values); the daily maxima from 2026-08-01 to 10-14 in three requests (a single one is the API's "Range Limit Exceeded ... 31 days"); the verified hourly heights and highs and lows of 2025-01-01; the hourly predictions for 2026-10-16 at the nearest station of a point at the tip of Manhattan; the hourly heights of 2026-10-14, not yet verified ("No data was found": no values). The fixtures in Tests/Fixtures/ are its answers, the stations' list cut to five around New York.

Again on 2026-10-27: The Battery by its id, its water levels and predicted highs and lows of 2026-10-26, the hourly predictions of 2026-10-28 at the nearest station of a point (the example above).

Not verified: the API's throttling (no refusal met at half a second between calls), the other datums than MLLW and MSL's request shape, the Great Lakes' stations.

Responsibility

This package is an independent, unofficial client: it is not affiliated with, endorsed by or sponsored by NOAA or its Center for Operational Oceanographic Products and Services. CO-OPS's data are in the U.S. public domain; credit NOAA CO-OPS (Tides and Currents); recent water levels are preliminary, the predictions astronomical (no weather); space the calls and download only what is needed, as the README's Terms of use says. Whoever runs the package is responsible for any use made of what it returns: see the README's Responsibility section.

Proposer une modification docs/index.md