← 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,
        })),
      };
    },
  },
};