← back to Ventura Claw
server/connectors/nws.js
55 lines
// US National Weather Service — public, no key. UA required.
// https://www.weather.gov/documentation/services-web-api
const BASE = "https://api.weather.gov";
const UA = "VenturaClaw/1.0 (steve@venturaclaw.com)";
async function call(path) {
const r = await fetch(`${BASE}${path}`, {
headers: { "User-Agent": UA, Accept: "application/geo+json" },
signal: AbortSignal.timeout(15_000),
});
if (!r.ok) throw new Error(`nws ${r.status}`);
return r.json();
}
module.exports = {
meta: { id: "nws", name: "NWS Weather", category: "data", docsUrl: "https://www.weather.gov/documentation/services-web-api", auth: "none", realImpl: true },
fields: [],
configured() { return true; },
async health() {
try {
// Pull a known LA gridpoint (Steve's home territory)
const d = await call("/points/34.0522,-118.2437");
return { ok: true, label: `live · LA gridpoint office=${d.properties?.gridId || "?"}` };
} catch (e) { return { ok: false, reason: e.message }; }
},
actions: {
async "forecast"({ lat, lon, hourly }) {
if (lat == null || lon == null) throw new Error("lat + lon required");
const pts = await call(`/points/${lat},${lon}`);
const url = hourly ? pts.properties?.forecastHourly : pts.properties?.forecast;
if (!url) throw new Error("no forecast url for that location");
const f = await fetch(url, { headers: { "User-Agent": UA }, signal: AbortSignal.timeout(15_000) }).then(r => r.json());
return {
location: pts.properties?.relativeLocation?.properties,
periods: (f.properties?.periods || []).slice(0, hourly ? 24 : 7).map(p => ({
name: p.name, startTime: p.startTime, temperature: p.temperature, temperatureUnit: p.temperatureUnit,
windSpeed: p.windSpeed, windDirection: p.windDirection, shortForecast: p.shortForecast, detailedForecast: p.detailedForecast,
})),
};
},
async "alerts"({ area, point }) {
const qs = new URLSearchParams();
if (area) qs.set("area", area); // 2-letter state code
if (point) qs.set("point", point); // "lat,lon"
const d = await call(`/alerts/active?${qs}`);
return {
count: (d.features || []).length,
alerts: (d.features || []).slice(0, 25).map(f => ({
headline: f.properties?.headline, severity: f.properties?.severity,
urgency: f.properties?.urgency, event: f.properties?.event, areaDesc: f.properties?.areaDesc,
effective: f.properties?.effective, expires: f.properties?.expires,
})),
};
},
},
};