Updated documentation and added API.md to document the stable external wire format.
This commit is contained in:
332
API.md
Normal file
332
API.md
Normal file
@@ -0,0 +1,332 @@
|
||||
# weatherfeeder API (Wire Contract)
|
||||
|
||||
This document defines the stable, consumer-facing JSON contract emitted by weatherfeeder sinks.
|
||||
|
||||
weatherfeeder emits **events** encoded as JSON. Each event has:
|
||||
- an **envelope** (metadata + schema identifier), and
|
||||
- a **payload** whose shape is determined by `schema`.
|
||||
|
||||
Downstream consumers should:
|
||||
1. parse the event envelope,
|
||||
2. switch on `schema`, then
|
||||
3. decode `payload` into the matching schema.
|
||||
|
||||
---
|
||||
|
||||
## Event envelope
|
||||
|
||||
All events are JSON objects with these fields:
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---:|:---:|---|
|
||||
| `id` | string | yes | Stable event identifier. Treat as opaque. |
|
||||
| `schema` | string | yes | Schema identifier (e.g. `weather.observation.v1`). |
|
||||
| `source` | string | yes | Provider/source identifier (stable within configuration). |
|
||||
| `effectiveAt` | string (timestamp) | yes | RFC3339Nano timestamp indicating when this event is effective. |
|
||||
| `payload` | object | yes | Schema-specific payload (see below). |
|
||||
|
||||
### Timestamp format
|
||||
|
||||
All timestamps are encoded as JSON strings using Go’s `time.Time` JSON encoding (RFC3339Nano).
|
||||
|
||||
Examples:
|
||||
- `"2026-01-17T14:27:00Z"`
|
||||
- `"2026-01-17T08:27:00-06:00"`
|
||||
|
||||
---
|
||||
|
||||
## Canonical schemas
|
||||
|
||||
weatherfeeder emits three canonical domain schemas:
|
||||
|
||||
- `weather.observation.v1`
|
||||
- `weather.forecast.v1`
|
||||
- `weather.alert.v1`
|
||||
|
||||
Each payload is described below using the JSON field names as the contract.
|
||||
|
||||
---
|
||||
|
||||
## Shared conventions
|
||||
|
||||
### Optional fields
|
||||
|
||||
Most non-identity measurements are optional. Optional fields are omitted when unknown (i.e., not present).
|
||||
|
||||
### Units
|
||||
|
||||
Canonical payloads are normalized to metric units:
|
||||
|
||||
- Temperature: `*C` (Celsius)
|
||||
- Wind speed/gust: `*Kmh` (kilometers/hour)
|
||||
- Pressure: `*Pa` (Pascals)
|
||||
- Visibility / distance / elevation: `*Meters` (meters)
|
||||
- Precipitation amount / snowfall depth: `*Mm` (millimeters)
|
||||
- Humidity / cloud cover / PoP: `*Percent` (0–100)
|
||||
|
||||
### Float rounding
|
||||
|
||||
For readability and stability, weatherfeeder rounds floating-point values in canonical payloads to
|
||||
**2 digits after the decimal** during normalization finalization.
|
||||
|
||||
### WMO condition codes
|
||||
|
||||
`conditionCode` uses the WMO weather interpretation code vocabulary.
|
||||
|
||||
- Type: integer
|
||||
- Unknown/unmappable: `-1`
|
||||
|
||||
Downstream consumers should treat unknown codes as “unknown conditions” rather than failing decoding.
|
||||
|
||||
---
|
||||
|
||||
## Schema: `weather.observation.v1`
|
||||
|
||||
Payload type: `WeatherObservation`
|
||||
|
||||
A `WeatherObservation` represents a point-in-time observation for a station/location.
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Type | Required | Units / Notes |
|
||||
|---|---:|:---:|---|
|
||||
| `stationId` | string | no | Provider station/location identifier |
|
||||
| `stationName` | string | no | Human name, if available |
|
||||
| `timestamp` | string (timestamp) | yes | Observation time |
|
||||
| `conditionCode` | int | yes | WMO code (`-1` for unknown) |
|
||||
| `conditionText` | string | no | Canonical short text (often derived from WMO code) |
|
||||
| `isDay` | bool | no | Day/night hint when available |
|
||||
| `providerRawDescription` | string | no | Provider-specific “evidence” text |
|
||||
| `textDescription` | string | no | Legacy/transitional human text |
|
||||
| `iconUrl` | string | no | Legacy/transitional icon URL |
|
||||
| `temperatureC` | number | no | °C |
|
||||
| `dewpointC` | number | no | °C |
|
||||
| `windDirectionDegrees` | number | no | Degrees (meteorological) |
|
||||
| `windSpeedKmh` | number | no | km/h |
|
||||
| `windGustKmh` | number | no | km/h |
|
||||
| `barometricPressurePa` | number | no | Pa |
|
||||
| `seaLevelPressurePa` | number | no | Pa |
|
||||
| `visibilityMeters` | number | no | meters |
|
||||
| `relativeHumidityPercent` | number | no | percent (0–100) |
|
||||
| `apparentTemperatureC` | number | no | °C |
|
||||
| `elevationMeters` | number | no | meters |
|
||||
| `rawMessage` | string | no | Provider raw message (e.g. METAR), if available |
|
||||
| `presentWeather` | array | no | Provider-specific structured fragments |
|
||||
| `cloudLayers` | array | no | Cloud layers (base + amount) |
|
||||
|
||||
### Nested: `cloudLayers[]`
|
||||
|
||||
Each `cloudLayers[]` element:
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---:|:---:|---|
|
||||
| `baseMeters` | number | no | Cloud base altitude in meters |
|
||||
| `amount` | string | no | Provider string (e.g. FEW/SCT/BKN/OVC) |
|
||||
|
||||
### Nested: `presentWeather[]`
|
||||
|
||||
Each `presentWeather[]` element:
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---:|:---:|---|
|
||||
| `raw` | object | no | Provider-specific JSON object |
|
||||
|
||||
---
|
||||
|
||||
## Schema: `weather.forecast.v1`
|
||||
|
||||
Payload type: `WeatherForecastRun`
|
||||
|
||||
A `WeatherForecastRun` is a single issued forecast snapshot for a location and a specific product
|
||||
(hourly / narrative / daily). The run contains an ordered list of forecast periods.
|
||||
|
||||
### `product` values
|
||||
|
||||
`product` is one of:
|
||||
|
||||
- `"hourly"`
|
||||
- `"narrative"`
|
||||
- `"daily"`
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---:|:---:|---|
|
||||
| `locationId` | string | no | Provider location identifier |
|
||||
| `locationName` | string | no | Human name, if available |
|
||||
| `issuedAt` | string (timestamp) | yes | When this run was generated/issued |
|
||||
| `updatedAt` | string (timestamp) | no | Optional later update time |
|
||||
| `product` | string | yes | `"hourly"`, `"narrative"`, or `"daily"` |
|
||||
| `latitude` | number | no | Degrees |
|
||||
| `longitude` | number | no | Degrees |
|
||||
| `elevationMeters` | number | no | meters |
|
||||
| `periods` | array | yes | Chronological forecast periods |
|
||||
|
||||
### Nested: `periods[]` (`WeatherForecastPeriod`)
|
||||
|
||||
A `WeatherForecastPeriod` is valid for `[startTime, endTime)`.
|
||||
|
||||
| Field | Type | Required | Units / Notes |
|
||||
|---|---:|:---:|---|
|
||||
| `startTime` | string (timestamp) | yes | Period start |
|
||||
| `endTime` | string (timestamp) | yes | Period end |
|
||||
| `name` | string | no | Human label (often empty for hourly) |
|
||||
| `isDay` | bool | no | Day/night hint |
|
||||
| `conditionCode` | int | yes | WMO code (`-1` for unknown) |
|
||||
| `conditionText` | string | no | Canonical short text |
|
||||
| `providerRawDescription` | string | no | Provider-specific “evidence” text |
|
||||
| `textDescription` | string | no | Human-facing short phrase |
|
||||
| `detailedText` | string | no | Longer narrative |
|
||||
| `iconUrl` | string | no | Legacy/transitional |
|
||||
| `temperatureC` | number | no | °C |
|
||||
| `temperatureCMin` | number | no | °C (aggregated products) |
|
||||
| `temperatureCMax` | number | no | °C (aggregated products) |
|
||||
| `dewpointC` | number | no | °C |
|
||||
| `relativeHumidityPercent` | number | no | percent |
|
||||
| `windDirectionDegrees` | number | no | degrees |
|
||||
| `windSpeedKmh` | number | no | km/h |
|
||||
| `windGustKmh` | number | no | km/h |
|
||||
| `barometricPressurePa` | number | no | Pa |
|
||||
| `visibilityMeters` | number | no | meters |
|
||||
| `apparentTemperatureC` | number | no | °C |
|
||||
| `cloudCoverPercent` | number | no | percent |
|
||||
| `probabilityOfPrecipitationPercent` | number | no | percent |
|
||||
| `precipitationAmountMm` | number | no | mm (liquid equivalent) |
|
||||
| `snowfallDepthMm` | number | no | mm |
|
||||
| `uvIndex` | number | no | unitless index |
|
||||
|
||||
---
|
||||
|
||||
## Schema: `weather.alert.v1`
|
||||
|
||||
Payload type: `WeatherAlertRun`
|
||||
|
||||
A `WeatherAlertRun` is a snapshot of *active* alerts for a location as-of a point in time.
|
||||
A run may contain zero, one, or many alerts.
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---:|:---:|---|
|
||||
| `locationId` | string | no | Provider location identifier |
|
||||
| `locationName` | string | no | Human name, if available |
|
||||
| `asOf` | string (timestamp) | yes | When the provider asserted this snapshot is current |
|
||||
| `latitude` | number | no | Degrees |
|
||||
| `longitude` | number | no | Degrees |
|
||||
| `alerts` | array | yes | Active alerts (order provider-dependent) |
|
||||
|
||||
### Nested: `alerts[]` (`WeatherAlert`)
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---:|:---:|---|
|
||||
| `id` | string | yes | Provider-stable identifier (often a URL/URI) |
|
||||
| `event` | string | no | Classification |
|
||||
| `headline` | string | no | Short headline |
|
||||
| `severity` | string | no | e.g. Extreme/Severe/Moderate/Minor/Unknown |
|
||||
| `urgency` | string | no | e.g. Immediate/Expected/Future/Past/Unknown |
|
||||
| `certainty` | string | no | e.g. Observed/Likely/Possible/Unlikely/Unknown |
|
||||
| `status` | string | no | e.g. Actual/Exercise/Test/System/Unknown |
|
||||
| `messageType` | string | no | e.g. Alert/Update/Cancel |
|
||||
| `category` | string | no | e.g. Met/Geo/Safety/... |
|
||||
| `response` | string | no | e.g. Shelter/Evacuate/Prepare/... |
|
||||
| `description` | string | no | Narrative |
|
||||
| `instruction` | string | no | What to do |
|
||||
| `sent` | string (timestamp) | no | Provider-dependent |
|
||||
| `effective` | string (timestamp) | no | Provider-dependent |
|
||||
| `onset` | string (timestamp) | no | Provider-dependent |
|
||||
| `expires` | string (timestamp) | no | Provider-dependent |
|
||||
| `areaDescription` | string | no | Often a provider string |
|
||||
| `senderName` | string | no | Provenance |
|
||||
| `references` | array | no | Related alert references |
|
||||
|
||||
### Nested: `references[]` (`AlertReference`)
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---:|:---:|---|
|
||||
| `id` | string | no | Provider reference ID/URI |
|
||||
| `identifier` | string | no | Provider identifier string, if distinct |
|
||||
| `sender` | string | no | Sender |
|
||||
| `sent` | string (timestamp) | no | Timestamp |
|
||||
|
||||
---
|
||||
|
||||
## Compatibility rules
|
||||
|
||||
- Consumers **must** ignore unknown fields.
|
||||
- Producers (weatherfeeder) prefer **additive changes** within a schema version.
|
||||
- Renames/removals/semantic breaks require a **schema version bump** (`weather.*.v2`).
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Observation event (`weather.observation.v1`)
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "nws:KSTL:2026-01-17T14:00:00Z",
|
||||
"schema": "weather.observation.v1",
|
||||
"source": "nws_observation",
|
||||
"effectiveAt": "2026-01-17T14:00:00Z",
|
||||
"payload": {
|
||||
"stationId": "KSTL",
|
||||
"timestamp": "2026-01-17T14:00:00Z",
|
||||
"conditionCode": 1,
|
||||
"conditionText": "Mainly Sunny",
|
||||
"temperatureC": 3.25,
|
||||
"windSpeedKmh": 18.5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Forecast event (`weather.forecast.v1`)
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "openmeteo:38.63,-90.20:2026-01-17T13:00:00Z",
|
||||
"schema": "weather.forecast.v1",
|
||||
"source": "openmeteo_forecast",
|
||||
"effectiveAt": "2026-01-17T13:00:00Z",
|
||||
"payload": {
|
||||
"locationName": "St. Louis, MO",
|
||||
"issuedAt": "2026-01-17T13:00:00Z",
|
||||
"product": "hourly",
|
||||
"latitude": 38.63,
|
||||
"longitude": -90.2,
|
||||
"periods": [
|
||||
{
|
||||
"startTime": "2026-01-17T14:00:00Z",
|
||||
"endTime": "2026-01-17T15:00:00Z",
|
||||
"conditionCode": 2,
|
||||
"conditionText": "Partly Cloudy",
|
||||
"temperatureC": 3.5,
|
||||
"probabilityOfPrecipitationPercent": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Alert event (`weather.alert.v1`)
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "nws:alerts:2026-01-17T14:10:00Z",
|
||||
"schema": "weather.alert.v1",
|
||||
"source": "nws_alerts",
|
||||
"effectiveAt": "2026-01-17T14:10:00Z",
|
||||
"payload": {
|
||||
"asOf": "2026-01-17T14:05:00Z",
|
||||
"alerts": [
|
||||
{
|
||||
"id": "https://api.weather.gov/alerts/abc123",
|
||||
"event": "Winter Weather Advisory",
|
||||
"headline": "Winter Weather Advisory issued January 17 at 8:05AM CST",
|
||||
"severity": "Moderate",
|
||||
"description": "Mixed precipitation expected...",
|
||||
"expires": "2026-01-18T06:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
33
README.md
33
README.md
@@ -1,3 +1,34 @@
|
||||
# weatherfeeder
|
||||
|
||||
A small daemon to poll weather observations, alerts, and forecasts from a variety of sources.
|
||||
weatherfeeder is a small daemon that polls weather observations, forecasts, and alerts from multiple upstream
|
||||
providers, normalizes them into a provider-independent format, and emits them to a sink.
|
||||
|
||||
Today, the only implemented sink is `stdout`, which prints JSON-encoded events.
|
||||
|
||||
## What weatherfeeder emits
|
||||
|
||||
weatherfeeder emits **feed events** encoded as JSON. Each event includes a schema identifier and a payload.
|
||||
Downstream consumers should key off the `schema` value and decode the `payload` accordingly.
|
||||
|
||||
Canonical domain schemas emitted after normalization:
|
||||
|
||||
- `weather.observation.v1` → `WeatherObservation`
|
||||
- `weather.forecast.v1` → `WeatherForecastRun`
|
||||
- `weather.alert.v1` → `WeatherAlertRun`
|
||||
|
||||
For the complete wire contract (event envelope + payload schemas, fields, units, and compatibility rules), see:
|
||||
|
||||
- **API.md**
|
||||
|
||||
## Upstream providers (current MVP)
|
||||
|
||||
- NWS: observations, hourly forecasts, alerts
|
||||
- Open-Meteo: observations, hourly forecasts
|
||||
- OpenWeather: observations
|
||||
|
||||
## Versioning & compatibility
|
||||
|
||||
The JSON field names on canonical payload types are treated as part of the wire contract.
|
||||
Additive changes are preferred. Renames/removals require a schema version bump.
|
||||
|
||||
See **API.md** for details.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
---
|
||||
sources:
|
||||
# - name: NWSObservationKSTL
|
||||
# kind: observation
|
||||
# driver: nws_observation
|
||||
# every: 10m
|
||||
# params:
|
||||
# url: "https://api.weather.gov/stations/KSTL/observations/latest"
|
||||
# user_agent: "HomeOps (eric@maximumdirect.net)"
|
||||
- name: NWSObservationKSTL
|
||||
kind: observation
|
||||
driver: nws_observation
|
||||
every: 10m
|
||||
params:
|
||||
url: "https://api.weather.gov/stations/KSTL/observations/latest"
|
||||
user_agent: "HomeOps (eric@maximumdirect.net)"
|
||||
|
||||
# - name: OpenMeteoObservation
|
||||
# kind: observation
|
||||
|
||||
@@ -111,7 +111,7 @@ type WeatherForecastPeriod struct {
|
||||
// Quantitative precip is not universally available, but OpenWeather/Open-Meteo often supply it.
|
||||
// Use liquid-equivalent mm for interoperability.
|
||||
PrecipitationAmountMm *float64 `json:"precipitationAmountMm,omitempty"`
|
||||
SnowfallDepthMM *float64 `json:"SnowfallDepthMM,omitempty"`
|
||||
SnowfallDepthMM *float64 `json:"snowfallDepthMm,omitempty"`
|
||||
|
||||
// Optional extras that some providers supply and downstream might care about.
|
||||
UVIndex *float64 `json:"uvIndex,omitempty"`
|
||||
|
||||
Reference in New Issue
Block a user