Skip to content

Getting started

This page takes you from a weather station and a website to a live wind card on that website. You mount a feed handler on your server, check that it answers, then render components against it. Adapters, theming, React, and the wire contract all build on these steps.

Terminal window
pnpm add @azohra/meteo.station

List your stations and create a handler. Each entry names its vendor, and the matching adapter reads that hardware and converts its readings to the wire contract.

import { createStationFeedHandler } from "@azohra/meteo.station/server";
// Every station below is fictional — substitute your own identifiers.
const handler = createStationFeedHandler({
stations: [
{ vendor: "windnerd", id: "bluff", name: "Bluff Launch",
stationKey: "bluff-launch", locationId: 8675 },
{ vendor: "tempest", id: "meadow", name: "Ridge Meadow",
stationId: 12345, token: process.env.TEMPEST_TOKEN! },
{ vendor: "campbell", id: "summit", name: "Summit Logger",
baseUrl: "http://logger.example:30001/.", source: "LOGGER01:Wind Station",
timeZone: "America/Vancouver", latitude: 49.5, longitude: -118.5 },
{ vendor: "ecowitt", id: "yard", name: "Home Yard",
applicationKey: process.env.ECOWITT_APPLICATION_KEY!,
apiKey: process.env.ECOWITT_API_KEY!,
mac: "FF:FF:FF:FF:FF:FF", elevationM: 1000 },
],
primaryStationId: "summit",
cors: true,
});
// Mount anywhere that speaks web-standard Request/Response — Node 22+,
// workers, Deno, or a framework route. Routing is by pathname suffix, so
// this page mounts everything under /api/wind — the same MOUNT BASE every
// render example below passes.
export default { fetch: handler }; // e.g. a Cloudflare worker

Each vendor’s page lists every field its entry takes and the quirks its adapter handles: WindNerd, Tempest, Campbell, and Ecowitt.

Terminal window
curl 'https://your.host/api/wind/feed' # every station + history
curl 'https://your.host/api/wind/feed?hours=2' # narrower window (≤ the ceiling)
curl 'https://your.host/api/wind/current?station=summit' # one station, reading only

/feed returns a StationFeed. It holds every configured station in one document, whether or not that station’s upstream answered. In this abbreviated example, … marks omitted fields; the field names are real.

{
"schemaVersion": 2,
"servedAt": "2026-08-05T22:13:00.000Z",
"primaryStationId": "summit",
"stations": [
{ "id": "bluff", "name": "Bluff Launch", "status": "ok",
"capabilities": { "gustLull": true, "history": true, "live": true, … },
"reading": { "observedAt": "2026-08-05T22:12:45.000Z",
"windAvgMps": 2.5, "windGustMps": 3.9, "windLullMps": 1.7,
"windDirectionDeg": 290, … },
"history": { "periodMinutes": 1, "points": [ … ] }, … },
{ "id": "meadow", "status": "unavailable", "reason": "upstream_error",
"reading": null, "history": null, … },
{ "id": "summit", "status": "ok", … }
]
}

The meadow entry shows a failed upstream. The station keeps its place in the feed with "status": "unavailable" and a machine-readable reason, and the other stations are unaffected.

?hours=2 returns the same shape with history cut to the last two hours. /current returns a StationCurrent, which holds one station’s reading and a null history:

{
"schemaVersion": 2,
"servedAt": "2026-08-05T22:13:00.000Z",
"station": { "id": "summit", "name": "Summit Logger", "status": "ok",
"reading": { "observedAt": "2026-08-05T22:12:57.000Z",
"windAvgMps": 2.5, … },
"history": null, … }
}

A third route, /live, streams raw samples for stations that declare the live capability. What your hardware shows lists which vendors have it. The wire contract describes every field, and station/schema/ holds annotated examples.

Import the default styles and the React components, poll the handler’s mount base, and render the card and table inside a provider:

import "@azohra/meteo.station/styles.css"; // the default skin (an intentional side effect)
import {
StationFeedProvider, useStation, StationCard, StationTable,
} from "@azohra/meteo.station/react";
function LiveWind() {
// The argument is the MOUNT BASE — where the handler is mounted. The hook
// polls `${base}/feed` AND `${base}/current?station=summit`, folds the
// fast reading into the full feed, and applies the freshness clock rule.
const { feed, receivedAtMs } = useStation("/api/wind", "summit", {
fetchInit: { cache: "no-store" },
});
if (!feed) return null;
return (
<div className="meteo-root">
<StationFeedProvider
feed={feed}
receivedAtMs={receivedAtMs}
thresholds={{ unit: "kmh", values: [12, 20, 28] }} // your wind vocabulary
unit="knots" // what the numbers wear
>
<StationCard /> {/* the feed's primary station, provider-fed */}
<StationTable /> {/* the whole fleet, no props re-threaded */}
</StationFeedProvider>
</div>
);
}

You should see a card for the primary station and a table of every station in the feed:

Launch RidgeWindNerd · 1245 m · updated 13:00LiveLULL11NESW17km/hGUST24fromNW 313°07:00 – 13:00 · lull–gust band · vanes point downwind01530122028TO————ESESESSSWWSWWWNWWNWNWNWNWavg1123581213171819181707:1208:4210:1211:4213:00

The history chart appears only for a station that declares history. WindNerd and Campbell do. Tempest and Ecowitt serve only the latest reading, so their cards show the dial and readouts without a chart. What your hardware shows maps each capability to the components that use it.

useStation polls the feed and adds a lighter /current poll for the station you name. useStationFeed(url) polls the feed alone. Hooks, composition, and seeding the provider during server-side rendering are covered in React, and the tokens behind the default styles are in Theming.

The custom-elements binding renders the same page with one module script and plain markup. <meteo-station-feed> polls the same endpoints through the same stores, and its children render the same DOM as the React components:

<script type="module">import "@azohra/meteo.station/elements/register";</script>
<meteo-station-feed src="/api/wind" thresholds='{"unit":"kmh","values":[12,20,28]}'>
<meteo-station-card></meteo-station-card>
<meteo-station-table></meteo-station-table>
</meteo-station-feed>

maxHistoryHours defaults to 6. It sets both the default history window and the largest value ?hours= accepts; the range and rejection rules are in the HTTP protocol.

Routes match by pathname suffix by default. When several handlers are mounted side by side, pass basePath: "/api/wind" to match exact routes (/api/wind/feed, /api/wind/current) instead.

Responses carry Cache-Control and a weak ETag, so a client revalidating an unchanged feed gets a 304. The HTTP protocol explains how both are derived. To set your own caching for a CDN:

cacheControl: (route, maxAge) =>
`public, max-age=${maxAge}, s-maxage=${maxAge}, stale-while-revalidate=30`,

stations can also be a function, such as a database or KV read. The handler calls it once each time it assembles a document, passing the Request when there is one:

createStationFeedHandler({ stations: async (request) => readStationsFromDb(request) });

A station whose config fails validation, or repeats another station’s id, becomes unavailable with reason not_configured, and the zod issues are logged. A bad row never makes the feed return a 500. A static array gets the same check once, when the handler is created; it logs a warning and does not throw.

The handler is a thin HTTP wrapper. Cron jobs, static builds, and framework loaders can call the functions beneath it:

import { loadStationFeed, loadStationCurrent } from "@azohra/meteo.station/server";
const feed = await loadStationFeed({ stations, historyHours: 3 }); // StationFeed
const current = await loadStationCurrent({ stations, stationId: "summit" }); // StationCurrent

Both set servedAt and schemaVersion, and both contain failures the same way the handler does: an adapter that throws marks its own station unavailable and leaves the rest of the document intact.

If you want toRead
Configure your vendor’s station entryYour vendor’s page under Adapters
Find out why a station has no chart or columnWhat your hardware shows
Build layouts beyond the card and tableReact, or custom elements without a framework
Match your site’s coloursTheming
Pull a season of WindNerd records at coarse record resolutionDirect-adapter options
Slice history yourselfClient data
See exactly what the handler sendsWire contract