@cyanheads/epa-mcp-server 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +13 -10
- package/CLAUDE.md +13 -10
- package/Dockerfile +108 -37
- package/README.md +67 -44
- package/changelog/0.3.x/0.3.3.md +34 -0
- package/changelog/0.4.x/0.4.0.md +35 -0
- package/dist/mcp-server/tools/definitions/get-air-quality.tool.d.ts +23 -9
- package/dist/mcp-server/tools/definitions/get-air-quality.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-air-quality.tool.js +166 -98
- package/dist/mcp-server/tools/definitions/get-air-quality.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-ejscreen.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-ejscreen.tool.js +1 -1
- package/dist/mcp-server/tools/definitions/get-ejscreen.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-facility.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-facility.tool.js +5 -14
- package/dist/mcp-server/tools/definitions/get-facility.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-tri-releases.tool.d.ts +2 -1
- package/dist/mcp-server/tools/definitions/get-tri-releases.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-tri-releases.tool.js +10 -14
- package/dist/mcp-server/tools/definitions/get-tri-releases.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/index.d.ts +51 -18
- package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/search-facilities.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/search-facilities.tool.js +9 -16
- package/dist/mcp-server/tools/definitions/search-facilities.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/search-superfund.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/search-superfund.tool.js +4 -14
- package/dist/mcp-server/tools/definitions/search-superfund.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/search-tri-releases.tool.d.ts +10 -3
- package/dist/mcp-server/tools/definitions/search-tri-releases.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/search-tri-releases.tool.js +50 -10
- package/dist/mcp-server/tools/definitions/search-tri-releases.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/search-violations.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/search-violations.tool.js +1 -3
- package/dist/mcp-server/tools/definitions/search-violations.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/search-water-systems.tool.d.ts +3 -1
- package/dist/mcp-server/tools/definitions/search-water-systems.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/search-water-systems.tool.js +23 -10
- package/dist/mcp-server/tools/definitions/search-water-systems.tool.js.map +1 -1
- package/dist/mcp-server/tools/format-lbs.d.ts +14 -0
- package/dist/mcp-server/tools/format-lbs.d.ts.map +1 -0
- package/dist/mcp-server/tools/format-lbs.js +16 -0
- package/dist/mcp-server/tools/format-lbs.js.map +1 -0
- package/dist/mcp-server/tools/format-location.d.ts +18 -0
- package/dist/mcp-server/tools/format-location.d.ts.map +1 -0
- package/dist/mcp-server/tools/format-location.js +26 -0
- package/dist/mcp-server/tools/format-location.js.map +1 -0
- package/dist/mcp-server/tools/format-tri-release.d.ts +16 -0
- package/dist/mcp-server/tools/format-tri-release.d.ts.map +1 -0
- package/dist/mcp-server/tools/format-tri-release.js +23 -0
- package/dist/mcp-server/tools/format-tri-release.js.map +1 -0
- package/dist/services/airnow/airnow-service.d.ts +10 -29
- package/dist/services/airnow/airnow-service.d.ts.map +1 -1
- package/dist/services/airnow/airnow-service.js +148 -95
- package/dist/services/airnow/airnow-service.js.map +1 -1
- package/dist/services/airnow/types.d.ts +62 -23
- package/dist/services/airnow/types.d.ts.map +1 -1
- package/dist/services/airnow/types.js +2 -1
- package/dist/services/airnow/types.js.map +1 -1
- package/dist/services/dmap/dmap-service.d.ts +41 -2
- package/dist/services/dmap/dmap-service.d.ts.map +1 -1
- package/dist/services/dmap/dmap-service.js +231 -116
- package/dist/services/dmap/dmap-service.js.map +1 -1
- package/dist/services/dmap/types.d.ts +17 -4
- package/dist/services/dmap/types.d.ts.map +1 -1
- package/dist/services/echo/echo-service.d.ts.map +1 -1
- package/dist/services/echo/echo-service.js +8 -5
- package/dist/services/echo/echo-service.js.map +1 -1
- package/dist/services/ejscreen/ejscreen-service.js +1 -1
- package/dist/services/ejscreen/ejscreen-service.js.map +1 -1
- package/package.json +11 -10
- package/server.json +16 -24
|
@@ -1,44 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview AirNow API service for
|
|
3
|
-
* Wraps www.airnowapi.org/aq
|
|
4
|
-
* AirNow API's
|
|
2
|
+
* @fileoverview AirNow API service for current observations and issued forecasts.
|
|
3
|
+
* Wraps the 2026 www.airnowapi.org/aq web services `observation/current/ziplatlong/`
|
|
4
|
+
* and `forecast/current/`. Responses cached at ~1 hour TTL per the AirNow API's
|
|
5
|
+
* recommendation to avoid rate-limiting.
|
|
5
6
|
* @module services/airnow/airnow-service
|
|
6
7
|
*/
|
|
7
8
|
import type { Context } from '@cyanheads/mcp-ts-core';
|
|
8
9
|
import type { AppConfig } from '@cyanheads/mcp-ts-core/config';
|
|
9
10
|
import type { StorageService } from '@cyanheads/mcp-ts-core/storage';
|
|
10
|
-
import type {
|
|
11
|
+
import type { AirNowLocation, AirQualityLookup } from './types.js';
|
|
11
12
|
export declare class AirNowService {
|
|
12
13
|
private readonly baseUrl;
|
|
13
14
|
private readonly apiKey;
|
|
14
15
|
constructor(_config: AppConfig, _storage: StorageService);
|
|
16
|
+
/** Current hourly observations: per pollutant, the closest monitor within AirNow's 50-mile boundary. */
|
|
17
|
+
getCurrent(location: AirNowLocation, ctx: Context): Promise<AirQualityLookup>;
|
|
18
|
+
/** Every forecast day the location's reporting area has issued. */
|
|
19
|
+
getForecast(location: AirNowLocation, ctx: Context): Promise<AirQualityLookup>;
|
|
15
20
|
private buildUrl;
|
|
16
21
|
private cacheKey;
|
|
17
|
-
private
|
|
18
|
-
/** Get current air quality by ZIP code. */
|
|
19
|
-
getCurrentByZip(params: {
|
|
20
|
-
zipCode: string;
|
|
21
|
-
distanceMiles?: number;
|
|
22
|
-
}, ctx: Context): Promise<AirQualityResult[]>;
|
|
23
|
-
/** Get current air quality by lat/lng. */
|
|
24
|
-
getCurrentByLatLng(params: {
|
|
25
|
-
latitude: number;
|
|
26
|
-
longitude: number;
|
|
27
|
-
distanceMiles?: number;
|
|
28
|
-
}, ctx: Context): Promise<AirQualityResult[]>;
|
|
29
|
-
/** Get air quality forecast by ZIP code. */
|
|
30
|
-
getForecastByZip(params: {
|
|
31
|
-
zipCode: string;
|
|
32
|
-
date: string;
|
|
33
|
-
distanceMiles?: number;
|
|
34
|
-
}, ctx: Context): Promise<AirQualityResult[]>;
|
|
35
|
-
/** Get air quality forecast by lat/lng. */
|
|
36
|
-
getForecastByLatLng(params: {
|
|
37
|
-
latitude: number;
|
|
38
|
-
longitude: number;
|
|
39
|
-
date: string;
|
|
40
|
-
distanceMiles?: number;
|
|
41
|
-
}, ctx: Context): Promise<AirQualityResult[]>;
|
|
22
|
+
private fetchBody;
|
|
42
23
|
}
|
|
43
24
|
export declare function initAirNowService(config: AppConfig, storage: StorageService): void;
|
|
44
25
|
export declare function getAirNowService(): AirNowService;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"airnow-service.d.ts","sourceRoot":"","sources":["../../../src/services/airnow/airnow-service.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"airnow-service.d.ts","sourceRoot":"","sources":["../../../src/services/airnow/airnow-service.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAM/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAGrE,OAAO,KAAK,EACV,cAAc,EAEd,gBAAgB,EAGjB,MAAM,YAAY,CAAC;AAyHpB,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAEhC,YAAY,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,cAAc,EAWvD;IAED,wGAAwG;IAClG,UAAU,CAAC,QAAQ,EAAE,cAAc,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAKlF;IAED,mEAAmE;IAC7D,WAAW,CAAC,QAAQ,EAAE,cAAc,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAKnF;IAED,OAAO,CAAC,QAAQ;IAUhB,OAAO,CAAC,QAAQ;YAUF,SAAS;CA4DxB;AAMD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAElF;AAED,wBAAgB,gBAAgB,IAAI,aAAa,CAKhD"}
|
|
@@ -1,43 +1,122 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview AirNow API service for
|
|
3
|
-
* Wraps www.airnowapi.org/aq
|
|
4
|
-
* AirNow API's
|
|
2
|
+
* @fileoverview AirNow API service for current observations and issued forecasts.
|
|
3
|
+
* Wraps the 2026 www.airnowapi.org/aq web services `observation/current/ziplatlong/`
|
|
4
|
+
* and `forecast/current/`. Responses cached at ~1 hour TTL per the AirNow API's
|
|
5
|
+
* recommendation to avoid rate-limiting.
|
|
5
6
|
* @module services/airnow/airnow-service
|
|
6
7
|
*/
|
|
7
|
-
import { serviceUnavailable } from '@cyanheads/mcp-ts-core/errors';
|
|
8
|
+
import { serializationError, serviceUnavailable, validationError, } from '@cyanheads/mcp-ts-core/errors';
|
|
8
9
|
import { httpErrorFromResponse, withRetry } from '@cyanheads/mcp-ts-core/utils';
|
|
9
10
|
import { getServerConfig } from '../../config/server-config.js';
|
|
10
11
|
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
for
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
12
|
+
/** The six AirNow AQI descriptors (lowercased) and their category numbers. */
|
|
13
|
+
const AQI_CATEGORY_NUMBERS = new Map([
|
|
14
|
+
['good', 1],
|
|
15
|
+
['moderate', 2],
|
|
16
|
+
['unhealthy for sensitive groups', 3],
|
|
17
|
+
['unhealthy', 4],
|
|
18
|
+
['very unhealthy', 5],
|
|
19
|
+
['hazardous', 6],
|
|
20
|
+
]);
|
|
21
|
+
/** The message(s) of an `{"WebServiceError":[{"Message":…}]}` body; undefined for any other shape. */
|
|
22
|
+
function webServiceErrorMessage(body) {
|
|
23
|
+
if (typeof body !== 'object' || body === null || !('WebServiceError' in body))
|
|
24
|
+
return;
|
|
25
|
+
const entries = body.WebServiceError;
|
|
26
|
+
if (!Array.isArray(entries))
|
|
27
|
+
return;
|
|
28
|
+
const messages = entries.flatMap((entry) => typeof entry?.Message === 'string' && entry.Message.trim() !== '' ? [entry.Message] : []);
|
|
29
|
+
return messages.length > 0 ? messages.join(' ') : undefined;
|
|
30
|
+
}
|
|
31
|
+
/** Classify a 200 body. Anything but a record array or a WebServiceError message fails loudly. */
|
|
32
|
+
function parseBody(text) {
|
|
33
|
+
let body;
|
|
34
|
+
try {
|
|
35
|
+
body = JSON.parse(text);
|
|
36
|
+
}
|
|
37
|
+
catch (cause) {
|
|
38
|
+
throw serializationError('AirNow returned a response that is not JSON.', undefined, { cause });
|
|
39
|
+
}
|
|
40
|
+
if (Array.isArray(body))
|
|
41
|
+
return { rows: body };
|
|
42
|
+
const noDataMessage = webServiceErrorMessage(body);
|
|
43
|
+
if (noDataMessage !== undefined)
|
|
44
|
+
return { noDataMessage };
|
|
45
|
+
throw serializationError('AirNow returned an unrecognized response: expected an array of records or a WebServiceError message.');
|
|
46
|
+
}
|
|
47
|
+
/** An AQI value, or undefined when upstream sent none (forecasts use -1 for category-only). */
|
|
48
|
+
function validAqi(value) {
|
|
49
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
50
|
+
}
|
|
51
|
+
/** Group observation records by reporting area and observed hour; one reading per pollutant. */
|
|
52
|
+
function normalizeObservations(rows) {
|
|
53
|
+
const areas = new Map();
|
|
54
|
+
for (const r of rows) {
|
|
55
|
+
const aqi = validAqi(r.nowcastAQI);
|
|
56
|
+
if (!r.parameterName || (aqi === undefined && !r.aqiCategoryName))
|
|
57
|
+
continue;
|
|
58
|
+
const key = [r.reportingAreaName, r.dateObserved, r.hourObserved, r.localTimeZone].join('|');
|
|
59
|
+
let area = areas.get(key);
|
|
60
|
+
if (!area) {
|
|
61
|
+
area = {
|
|
62
|
+
...(r.reportingAreaName && { reportingArea: r.reportingAreaName }),
|
|
63
|
+
...(r.dateObserved && { dateObserved: r.dateObserved }),
|
|
64
|
+
...(r.hourObserved && { hourObserved: r.hourObserved }),
|
|
65
|
+
...(r.localTimeZone && { localTimeZone: r.localTimeZone }),
|
|
27
66
|
readings: [],
|
|
28
67
|
};
|
|
29
|
-
|
|
68
|
+
areas.set(key, area);
|
|
30
69
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
})
|
|
70
|
+
const categoryNumber = r.aqiCategoryName
|
|
71
|
+
? AQI_CATEGORY_NUMBERS.get(r.aqiCategoryName.trim().toLowerCase())
|
|
72
|
+
: undefined;
|
|
73
|
+
area.readings.push({
|
|
74
|
+
parameterName: r.parameterName,
|
|
75
|
+
...(aqi !== undefined && { aqi }),
|
|
76
|
+
...(r.aqiCategoryName && { categoryName: r.aqiCategoryName }),
|
|
77
|
+
...(categoryNumber !== undefined && { categoryNumber }),
|
|
78
|
+
...(r.siteName && { siteName: r.siteName }),
|
|
79
|
+
...(r.siteID && { siteID: r.siteID }),
|
|
80
|
+
...(r.reportingAgency && { reportingAgency: r.reportingAgency }),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return [...areas.values()];
|
|
84
|
+
}
|
|
85
|
+
/** Group forecast records by reporting area; one reading per pollutant per valid date. */
|
|
86
|
+
function normalizeForecasts(rows) {
|
|
87
|
+
const areas = new Map();
|
|
88
|
+
for (const r of rows) {
|
|
89
|
+
const aqi = validAqi(r.aqi);
|
|
90
|
+
if (!r.parameterName ||
|
|
91
|
+
(aqi === undefined && !r.categoryName && r.categoryNumber === undefined)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const key = r.reportingAreaCode ?? r.reportingArea ?? '';
|
|
95
|
+
let area = areas.get(key);
|
|
96
|
+
if (!area) {
|
|
97
|
+
area = {
|
|
98
|
+
...(r.reportingArea && { reportingArea: r.reportingArea }),
|
|
99
|
+
...(r.reportingAreaCode && { reportingAreaCode: r.reportingAreaCode }),
|
|
100
|
+
...(r.stateCode && { stateCode: r.stateCode }),
|
|
101
|
+
readings: [],
|
|
102
|
+
};
|
|
103
|
+
areas.set(key, area);
|
|
38
104
|
}
|
|
105
|
+
// AirNow repeats the area's discussion on every row; keep it once, verbatim.
|
|
106
|
+
if (!area.discussion && r.discussion)
|
|
107
|
+
area.discussion = r.discussion;
|
|
108
|
+
area.readings.push({
|
|
109
|
+
parameterName: r.parameterName,
|
|
110
|
+
...(aqi !== undefined && { aqi }),
|
|
111
|
+
...(r.categoryNumber !== undefined && { categoryNumber: r.categoryNumber }),
|
|
112
|
+
...(r.categoryName && { categoryName: r.categoryName }),
|
|
113
|
+
...(r.dateIssue && { dateIssue: r.dateIssue }),
|
|
114
|
+
...(r.dateValid && { dateValid: r.dateValid }),
|
|
115
|
+
...(r.forecastAgency && { forecastAgency: r.forecastAgency }),
|
|
116
|
+
...(typeof r.actionDay === 'boolean' && { actionDay: r.actionDay }),
|
|
117
|
+
});
|
|
39
118
|
}
|
|
40
|
-
return
|
|
119
|
+
return [...areas.values()];
|
|
41
120
|
}
|
|
42
121
|
export class AirNowService {
|
|
43
122
|
baseUrl;
|
|
@@ -52,20 +131,31 @@ export class AirNowService {
|
|
|
52
131
|
this.baseUrl = serverConfig.airNowBaseUrl;
|
|
53
132
|
this.apiKey = serverConfig.airNowApiKey;
|
|
54
133
|
}
|
|
134
|
+
/** Current hourly observations: per pollutant, the closest monitor within AirNow's 50-mile boundary. */
|
|
135
|
+
async getCurrent(location, ctx) {
|
|
136
|
+
const body = await this.fetchBody('observation', location, ctx);
|
|
137
|
+
return 'noDataMessage' in body
|
|
138
|
+
? { areas: [], noDataMessage: body.noDataMessage }
|
|
139
|
+
: { areas: normalizeObservations(body.rows) };
|
|
140
|
+
}
|
|
141
|
+
/** Every forecast day the location's reporting area has issued. */
|
|
142
|
+
async getForecast(location, ctx) {
|
|
143
|
+
const body = await this.fetchBody('forecast', location, ctx);
|
|
144
|
+
return 'noDataMessage' in body
|
|
145
|
+
? { areas: [], noDataMessage: body.noDataMessage }
|
|
146
|
+
: { areas: normalizeForecasts(body.rows) };
|
|
147
|
+
}
|
|
55
148
|
buildUrl(path, params) {
|
|
56
149
|
const url = new URL(`${this.baseUrl}/${path}`);
|
|
57
150
|
url.searchParams.set('format', 'application/json');
|
|
58
151
|
url.searchParams.set('API_KEY', this.apiKey);
|
|
59
152
|
for (const [key, val] of Object.entries(params)) {
|
|
60
|
-
|
|
61
|
-
url.searchParams.set(key, String(val));
|
|
62
|
-
}
|
|
153
|
+
url.searchParams.set(key, String(val));
|
|
63
154
|
}
|
|
64
155
|
return url.toString();
|
|
65
156
|
}
|
|
66
157
|
cacheKey(kind, params) {
|
|
67
158
|
const stable = Object.entries(params)
|
|
68
|
-
.filter(([, v]) => v !== undefined)
|
|
69
159
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
70
160
|
.map(([k, v]) => `${k}-${String(v).replace(/[^a-zA-Z0-9_.\-/]/g, '_')}`)
|
|
71
161
|
.join('_');
|
|
@@ -73,15 +163,33 @@ export class AirNowService {
|
|
|
73
163
|
const safeKind = kind.replace(/:/g, '/');
|
|
74
164
|
return `airnow/${safeKind}/${stable}`;
|
|
75
165
|
}
|
|
76
|
-
async
|
|
77
|
-
|
|
166
|
+
async fetchBody(service, location, ctx) {
|
|
167
|
+
const params = location.kind === 'zip'
|
|
168
|
+
? { zipCode: location.zipCode }
|
|
169
|
+
: { latitude: location.latitude, longitude: location.longitude };
|
|
170
|
+
const cacheKey = this.cacheKey(`${service}:${location.kind}`, params);
|
|
78
171
|
const cached = await ctx.state.get(cacheKey);
|
|
79
172
|
if (cached && cached.expiresAt > Date.now()) {
|
|
80
173
|
ctx.log.debug('AirNow cache hit', { cacheKey });
|
|
81
|
-
return cached.
|
|
174
|
+
return cached.body;
|
|
82
175
|
}
|
|
83
|
-
const
|
|
176
|
+
const path = service === 'observation' ? 'observation/current/ziplatlong/' : 'forecast/current/';
|
|
177
|
+
const url = this.buildUrl(path, params);
|
|
178
|
+
ctx.log.debug('AirNow request', { service, ...params });
|
|
179
|
+
const body = await withRetry(async () => {
|
|
84
180
|
const response = await fetch(url, { signal: ctx.signal });
|
|
181
|
+
// A ZIP is the only caller-supplied input on a ZIP request, so a 400 rejects it.
|
|
182
|
+
if (response.status === 400 && location.kind === 'zip') {
|
|
183
|
+
const upstream = await response.text();
|
|
184
|
+
let detail;
|
|
185
|
+
try {
|
|
186
|
+
detail = webServiceErrorMessage(JSON.parse(upstream));
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// Non-JSON 400 body: report the status alone.
|
|
190
|
+
}
|
|
191
|
+
throw validationError(`AirNow rejected zip_code "${location.zipCode}": ${detail ?? 'HTTP 400'}.`, { reason: 'invalid_zip_code', zipCode: location.zipCode });
|
|
192
|
+
}
|
|
85
193
|
if (!response.ok) {
|
|
86
194
|
throw await httpErrorFromResponse(response, { service: 'AirNow' });
|
|
87
195
|
}
|
|
@@ -89,69 +197,14 @@ export class AirNowService {
|
|
|
89
197
|
if (/^\s*<(!DOCTYPE\s+html|html[\s>])/i.test(text)) {
|
|
90
198
|
throw serviceUnavailable('AirNow API returned HTML instead of JSON — likely rate-limited.');
|
|
91
199
|
}
|
|
92
|
-
|
|
93
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
200
|
+
return parseBody(text);
|
|
94
201
|
}, {
|
|
95
202
|
operation: 'AirNowService.fetch',
|
|
96
203
|
baseDelayMs: 2000,
|
|
97
204
|
signal: ctx.signal,
|
|
98
205
|
});
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return data;
|
|
102
|
-
}
|
|
103
|
-
/** Get current air quality by ZIP code. */
|
|
104
|
-
async getCurrentByZip(params, ctx) {
|
|
105
|
-
const qparams = { zipCode: params.zipCode, distance: params.distanceMiles ?? 25 };
|
|
106
|
-
const url = this.buildUrl('observation/zipCode/current/', qparams);
|
|
107
|
-
const ck = this.cacheKey('current:zip', qparams);
|
|
108
|
-
ctx.log.debug('AirNow current by ZIP', { zipCode: params.zipCode });
|
|
109
|
-
const records = await this.fetchWithCache(url, ck, ctx);
|
|
110
|
-
return normalizeRecords(records);
|
|
111
|
-
}
|
|
112
|
-
/** Get current air quality by lat/lng. */
|
|
113
|
-
async getCurrentByLatLng(params, ctx) {
|
|
114
|
-
const qparams = {
|
|
115
|
-
latitude: params.latitude,
|
|
116
|
-
longitude: params.longitude,
|
|
117
|
-
distance: params.distanceMiles ?? 25,
|
|
118
|
-
};
|
|
119
|
-
const url = this.buildUrl('observation/latLong/current/', qparams);
|
|
120
|
-
const ck = this.cacheKey('current:latlng', qparams);
|
|
121
|
-
ctx.log.debug('AirNow current by lat/lng', { lat: params.latitude, lng: params.longitude });
|
|
122
|
-
const records = await this.fetchWithCache(url, ck, ctx);
|
|
123
|
-
return normalizeRecords(records);
|
|
124
|
-
}
|
|
125
|
-
/** Get air quality forecast by ZIP code. */
|
|
126
|
-
async getForecastByZip(params, ctx) {
|
|
127
|
-
const qparams = {
|
|
128
|
-
zipCode: params.zipCode,
|
|
129
|
-
date: params.date,
|
|
130
|
-
distance: params.distanceMiles ?? 25,
|
|
131
|
-
};
|
|
132
|
-
const url = this.buildUrl('forecast/zipCode/', qparams);
|
|
133
|
-
const ck = this.cacheKey('forecast:zip', qparams);
|
|
134
|
-
ctx.log.debug('AirNow forecast by ZIP', { zipCode: params.zipCode, date: params.date });
|
|
135
|
-
const records = await this.fetchWithCache(url, ck, ctx);
|
|
136
|
-
return normalizeRecords(records);
|
|
137
|
-
}
|
|
138
|
-
/** Get air quality forecast by lat/lng. */
|
|
139
|
-
async getForecastByLatLng(params, ctx) {
|
|
140
|
-
const qparams = {
|
|
141
|
-
latitude: params.latitude,
|
|
142
|
-
longitude: params.longitude,
|
|
143
|
-
date: params.date,
|
|
144
|
-
distance: params.distanceMiles ?? 25,
|
|
145
|
-
};
|
|
146
|
-
const url = this.buildUrl('forecast/latLong/', qparams);
|
|
147
|
-
const ck = this.cacheKey('forecast:latlng', qparams);
|
|
148
|
-
ctx.log.debug('AirNow forecast by lat/lng', {
|
|
149
|
-
lat: params.latitude,
|
|
150
|
-
lng: params.longitude,
|
|
151
|
-
date: params.date,
|
|
152
|
-
});
|
|
153
|
-
const records = await this.fetchWithCache(url, ck, ctx);
|
|
154
|
-
return normalizeRecords(records);
|
|
206
|
+
await ctx.state.set(cacheKey, { body, expiresAt: Date.now() + CACHE_TTL_MS });
|
|
207
|
+
return body;
|
|
155
208
|
}
|
|
156
209
|
}
|
|
157
210
|
// --- Init/accessor pattern ---
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"airnow-service.js","sourceRoot":"","sources":["../../../src/services/airnow/airnow-service.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"airnow-service.js","sourceRoot":"","sources":["../../../src/services/airnow/airnow-service.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,eAAe,GAChB,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EAAE,qBAAqB,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAChF,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAS5D,MAAM,YAAY,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,SAAS;AAE9C,8EAA8E;AAC9E,MAAM,oBAAoB,GAAG,IAAI,GAAG,CAAC;IACnC,CAAC,MAAM,EAAE,CAAC,CAAC;IACX,CAAC,UAAU,EAAE,CAAC,CAAC;IACf,CAAC,gCAAgC,EAAE,CAAC,CAAC;IACrC,CAAC,WAAW,EAAE,CAAC,CAAC;IAChB,CAAC,gBAAgB,EAAE,CAAC,CAAC;IACrB,CAAC,WAAW,EAAE,CAAC,CAAC;CACjB,CAAC,CAAC;AAKH,sGAAsG;AACtG,SAAS,sBAAsB,CAAC,IAAa;IAC3C,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,CAAC,iBAAiB,IAAI,IAAI,CAAC;QAAE,OAAO;IACtF,MAAM,OAAO,GAAG,IAAI,CAAC,eAAe,CAAC;IACrC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,OAAO;IACpC,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CACzC,OAAO,KAAK,EAAE,OAAO,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CACzF,CAAC;IACF,OAAO,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAC9D,CAAC;AAED,kGAAkG;AAClG,SAAS,SAAS,CAAC,IAAY;IAC7B,IAAI,IAAa,CAAC;IAClB,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,kBAAkB,CAAC,8CAA8C,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IACjG,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAC/C,MAAM,aAAa,GAAG,sBAAsB,CAAC,IAAI,CAAC,CAAC;IACnD,IAAI,aAAa,KAAK,SAAS;QAAE,OAAO,EAAE,aAAa,EAAE,CAAC;IAC1D,MAAM,kBAAkB,CACtB,sGAAsG,CACvG,CAAC;AACJ,CAAC;AAED,+FAA+F;AAC/F,SAAS,QAAQ,CAAC,KAAyB;IACzC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AAC/F,CAAC;AAED,gGAAgG;AAChG,SAAS,qBAAqB,CAAC,IAAsB;IACnD,MAAM,KAAK,GAAG,IAAI,GAAG,EAA0B,CAAC;IAChD,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,MAAM,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;QACnC,IAAI,CAAC,CAAC,CAAC,aAAa,IAAI,CAAC,GAAG,KAAK,SAAS,IAAI,CAAC,CAAC,CAAC,eAAe,CAAC;YAAE,SAAS;QAC5E,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,iBAAiB,EAAE,CAAC,CAAC,YAAY,EAAE,CAAC,CAAC,YAAY,EAAE,CAAC,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC7F,IAAI,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC1B,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,IAAI,GAAG;gBACL,GAAG,CAAC,CAAC,CAAC,iBAAiB,IAAI,EAAE,aAAa,EAAE,CAAC,CAAC,iBAAiB,EAAE,CAAC;gBAClE,GAAG,CAAC,CAAC,CAAC,YAAY,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC,YAAY,EAAE,CAAC;gBACvD,GAAG,CAAC,CAAC,CAAC,YAAY,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC,YAAY,EAAE,CAAC;gBACvD,GAAG,CAAC,CAAC,CAAC,aAAa,IAAI,EAAE,aAAa,EAAE,CAAC,CAAC,aAAa,EAAE,CAAC;gBAC1D,QAAQ,EAAE,EAAE;aACb,CAAC;YACF,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACvB,CAAC;QACD,MAAM,cAAc,GAAG,CAAC,CAAC,eAAe;YACtC,CAAC,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC,CAAC,eAAe,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;YAClE,CAAC,CAAC,SAAS,CAAC;QACd,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;YACjB,aAAa,EAAE,CAAC,CAAC,aAAa;YAC9B,GAAG,CAAC,GAAG,KAAK,SAAS,IAAI,EAAE,GAAG,EAAE,CAAC;YACjC,GAAG,CAAC,CAAC,CAAC,eAAe,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC,eAAe,EAAE,CAAC;YAC7D,GAAG,CAAC,cAAc,KAAK,SAAS,IAAI,EAAE,cAAc,EAAE,CAAC;YACvD,GAAG,CAAC,CAAC,CAAC,QAAQ,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC;YAC3C,GAAG,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC;YACrC,GAAG,CAAC,CAAC,CAAC,eAAe,IAAI,EAAE,eAAe,EAAE,CAAC,CAAC,eAAe,EAAE,CAAC;SACjE,CAAC,CAAC;IACL,CAAC;IACD,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;AAC7B,CAAC;AAED,0FAA0F;AAC1F,SAAS,kBAAkB,CAAC,IAAmB;IAC7C,MAAM,KAAK,GAAG,IAAI,GAAG,EAA0B,CAAC;IAChD,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,MAAM,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAC5B,IACE,CAAC,CAAC,CAAC,aAAa;YAChB,CAAC,GAAG,KAAK,SAAS,IAAI,CAAC,CAAC,CAAC,YAAY,IAAI,CAAC,CAAC,cAAc,KAAK,SAAS,CAAC,EACxE,CAAC;YACD,SAAS;QACX,CAAC;QACD,MAAM,GAAG,GAAG,CAAC,CAAC,iBAAiB,IAAI,CAAC,CAAC,aAAa,IAAI,EAAE,CAAC;QACzD,IAAI,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC1B,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,IAAI,GAAG;gBACL,GAAG,CAAC,CAAC,CAAC,aAAa,IAAI,EAAE,aAAa,EAAE,CAAC,CAAC,aAAa,EAAE,CAAC;gBAC1D,GAAG,CAAC,CAAC,CAAC,iBAAiB,IAAI,EAAE,iBAAiB,EAAE,CAAC,CAAC,iBAAiB,EAAE,CAAC;gBACtE,GAAG,CAAC,CAAC,CAAC,SAAS,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC;gBAC9C,QAAQ,EAAE,EAAE;aACb,CAAC;YACF,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACvB,CAAC;QACD,6EAA6E;QAC7E,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,CAAC,CAAC,UAAU;YAAE,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC,UAAU,CAAC;QACrE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;YACjB,aAAa,EAAE,CAAC,CAAC,aAAa;YAC9B,GAAG,CAAC,GAAG,KAAK,SAAS,IAAI,EAAE,GAAG,EAAE,CAAC;YACjC,GAAG,CAAC,CAAC,CAAC,cAAc,KAAK,SAAS,IAAI,EAAE,cAAc,EAAE,CAAC,CAAC,cAAc,EAAE,CAAC;YAC3E,GAAG,CAAC,CAAC,CAAC,YAAY,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC,YAAY,EAAE,CAAC;YACvD,GAAG,CAAC,CAAC,CAAC,SAAS,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC;YAC9C,GAAG,CAAC,CAAC,CAAC,SAAS,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC;YAC9C,GAAG,CAAC,CAAC,CAAC,cAAc,IAAI,EAAE,cAAc,EAAE,CAAC,CAAC,cAAc,EAAE,CAAC;YAC7D,GAAG,CAAC,OAAO,CAAC,CAAC,SAAS,KAAK,SAAS,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC;SACpE,CAAC,CAAC;IACL,CAAC;IACD,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;AAC7B,CAAC;AAED,MAAM,OAAO,aAAa;IACP,OAAO,CAAS;IAChB,MAAM,CAAS;IAEhC,YAAY,OAAkB,EAAE,QAAwB;QACtD,MAAM,YAAY,GAAG,eAAe,EAAE,CAAC;QACvC,kFAAkF;QAClF,6EAA6E;QAC7E,IAAI,CAAC,YAAY,CAAC,YAAY,EAAE,CAAC;YAC/B,MAAM,IAAI,KAAK,CACb,uGAAuG,CACxG,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,OAAO,GAAG,YAAY,CAAC,aAAa,CAAC;QAC1C,IAAI,CAAC,MAAM,GAAG,YAAY,CAAC,YAAY,CAAC;IAC1C,CAAC;IAED,wGAAwG;IACxG,KAAK,CAAC,UAAU,CAAC,QAAwB,EAAE,GAAY;QACrD,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,aAAa,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC;QAChE,OAAO,eAAe,IAAI,IAAI;YAC5B,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,aAAa,EAAE,IAAI,CAAC,aAAa,EAAE;YAClD,CAAC,CAAC,EAAE,KAAK,EAAE,qBAAqB,CAAC,IAAI,CAAC,IAAwB,CAAC,EAAE,CAAC;IACtE,CAAC;IAED,mEAAmE;IACnE,KAAK,CAAC,WAAW,CAAC,QAAwB,EAAE,GAAY;QACtD,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,UAAU,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC;QAC7D,OAAO,eAAe,IAAI,IAAI;YAC5B,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,aAAa,EAAE,IAAI,CAAC,aAAa,EAAE;YAClD,CAAC,CAAC,EAAE,KAAK,EAAE,kBAAkB,CAAC,IAAI,CAAC,IAAqB,CAAC,EAAE,CAAC;IAChE,CAAC;IAEO,QAAQ,CAAC,IAAY,EAAE,MAAuC;QACpE,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,EAAE,CAAC,CAAC;QAC/C,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,EAAE,kBAAkB,CAAC,CAAC;QACnD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QAC7C,KAAK,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAChD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QACzC,CAAC;QACD,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAC;IACxB,CAAC;IAEO,QAAQ,CAAC,IAAY,EAAE,MAAuC;QACpE,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;aAClC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;aACtC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,oBAAoB,EAAE,GAAG,CAAC,EAAE,CAAC;aACvE,IAAI,CAAC,GAAG,CAAC,CAAC;QACb,uFAAuF;QACvF,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QACzC,OAAO,UAAU,QAAQ,IAAI,MAAM,EAAE,CAAC;IACxC,CAAC;IAEO,KAAK,CAAC,SAAS,CACrB,OAAmC,EACnC,QAAwB,EACxB,GAAY;QAEZ,MAAM,MAAM,GACV,QAAQ,CAAC,IAAI,KAAK,KAAK;YACrB,CAAC,CAAC,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE;YAC/B,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,QAAQ,EAAE,SAAS,EAAE,QAAQ,CAAC,SAAS,EAAE,CAAC;QACrE,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,CAAC,CAAC;QAEtE,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,KAAK,CAAC,GAAG,CAA0C,QAAQ,CAAC,CAAC;QACtF,IAAI,MAAM,IAAI,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;YAC5C,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,kBAAkB,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;YAChD,OAAO,MAAM,CAAC,IAAI,CAAC;QACrB,CAAC;QAED,MAAM,IAAI,GACR,OAAO,KAAK,aAAa,CAAC,CAAC,CAAC,iCAAiC,CAAC,CAAC,CAAC,mBAAmB,CAAC;QACtF,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACxC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,gBAAgB,EAAE,EAAE,OAAO,EAAE,GAAG,MAAM,EAAE,CAAC,CAAC;QAExD,MAAM,IAAI,GAAG,MAAM,SAAS,CAC1B,KAAK,IAAI,EAAE;YACT,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;YAC1D,iFAAiF;YACjF,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;gBACvD,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;gBACvC,IAAI,MAA0B,CAAC;gBAC/B,IAAI,CAAC;oBACH,MAAM,GAAG,sBAAsB,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;gBACxD,CAAC;gBAAC,MAAM,CAAC;oBACP,8CAA8C;gBAChD,CAAC;gBACD,MAAM,eAAe,CACnB,6BAA6B,QAAQ,CAAC,OAAO,MAAM,MAAM,IAAI,UAAU,GAAG,EAC1E,EAAE,MAAM,EAAE,kBAAkB,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,CAC1D,CAAC;YACJ,CAAC;YACD,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,MAAM,MAAM,qBAAqB,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;YACrE,CAAC;YACD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;YACnC,IAAI,mCAAmC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBACnD,MAAM,kBAAkB,CACtB,iEAAiE,CAClE,CAAC;YACJ,CAAC;YACD,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC;QACzB,CAAC,EACD;YACE,SAAS,EAAE,qBAAqB;YAChC,WAAW,EAAE,IAAI;YACjB,MAAM,EAAE,GAAG,CAAC,MAAM;SACnB,CACF,CAAC;QAEF,MAAM,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,YAAY,EAAE,CAAC,CAAC;QAC9E,OAAO,IAAI,CAAC;IACd,CAAC;CACF;AAED,gCAAgC;AAEhC,IAAI,QAAmC,CAAC;AAExC,MAAM,UAAU,iBAAiB,CAAC,MAAiB,EAAE,OAAuB;IAC1E,QAAQ,GAAG,IAAI,aAAa,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAChD,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,qEAAqE,CAAC,CAAC;IACzF,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -1,39 +1,78 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview AirNow API response types and normalized domain types
|
|
2
|
+
* @fileoverview AirNow API response types and normalized domain types for the 2026
|
|
3
|
+
* web services: `observation/current/ziplatlong/` and `forecast/current/`.
|
|
3
4
|
* @module services/airnow/types
|
|
4
5
|
*/
|
|
5
|
-
/**
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
6
|
+
/** Where to look up air quality: a ZIP code, or a latitude/longitude point. */
|
|
7
|
+
export type AirNowLocation = {
|
|
8
|
+
kind: 'zip';
|
|
9
|
+
zipCode: string;
|
|
10
|
+
} | {
|
|
11
|
+
kind: 'latlng';
|
|
12
|
+
latitude: number;
|
|
13
|
+
longitude: number;
|
|
14
|
+
};
|
|
15
|
+
/** Raw record from `observation/current/ziplatlong/` — one pollutant from its closest monitor. */
|
|
16
|
+
export interface RawObservation {
|
|
17
|
+
aqiCategoryName?: string;
|
|
18
|
+
dateObserved?: string;
|
|
19
|
+
/** Local hour as `"HH:00"`. */
|
|
20
|
+
hourObserved?: string;
|
|
21
|
+
localTimeZone?: string;
|
|
22
|
+
nowcastAQI?: number;
|
|
23
|
+
parameterName?: string;
|
|
24
|
+
reportingAgency?: string;
|
|
25
|
+
reportingAreaName?: string;
|
|
26
|
+
siteID?: string;
|
|
27
|
+
siteName?: string;
|
|
28
|
+
}
|
|
29
|
+
/** Raw record from `forecast/current/` — one pollutant for one valid date. */
|
|
30
|
+
export interface RawForecast {
|
|
31
|
+
actionDay?: boolean;
|
|
32
|
+
/** Forecast AQI, or `-1` when the agency issued a category only. */
|
|
33
|
+
aqi?: number;
|
|
34
|
+
categoryName?: string;
|
|
35
|
+
categoryNumber?: number;
|
|
36
|
+
dateIssue?: string;
|
|
37
|
+
dateValid?: string;
|
|
38
|
+
/** Agency discussion, repeated on every row of a reporting area; often empty. */
|
|
39
|
+
discussion?: string;
|
|
40
|
+
forecastAgency?: string;
|
|
41
|
+
parameterName?: string;
|
|
42
|
+
reportingArea?: string;
|
|
43
|
+
reportingAreaCode?: string;
|
|
44
|
+
stateCode?: string;
|
|
20
45
|
}
|
|
21
|
-
/** Normalized AQI reading for a single pollutant. */
|
|
46
|
+
/** Normalized AQI reading for a single pollutant (observation or forecast day). */
|
|
22
47
|
export interface AqiReading {
|
|
23
|
-
|
|
48
|
+
actionDay?: boolean;
|
|
49
|
+
/** Omitted when upstream supplied no AQI number (category-only forecast). */
|
|
50
|
+
aqi?: number;
|
|
24
51
|
categoryName?: string;
|
|
25
52
|
categoryNumber?: number;
|
|
53
|
+
dateIssue?: string;
|
|
54
|
+
dateValid?: string;
|
|
55
|
+
forecastAgency?: string;
|
|
26
56
|
parameterName: string;
|
|
57
|
+
reportingAgency?: string;
|
|
58
|
+
siteID?: string;
|
|
59
|
+
siteName?: string;
|
|
27
60
|
}
|
|
28
|
-
/** Normalized
|
|
29
|
-
export interface
|
|
61
|
+
/** Normalized readings for one reporting area (and, for observations, one observed hour). */
|
|
62
|
+
export interface AirQualityArea {
|
|
30
63
|
dateObserved?: string;
|
|
31
|
-
|
|
32
|
-
|
|
64
|
+
discussion?: string;
|
|
65
|
+
hourObserved?: string;
|
|
33
66
|
localTimeZone?: string;
|
|
34
|
-
longitude?: number;
|
|
35
67
|
readings: AqiReading[];
|
|
36
68
|
reportingArea?: string;
|
|
69
|
+
reportingAreaCode?: string;
|
|
37
70
|
stateCode?: string;
|
|
38
71
|
}
|
|
72
|
+
/** Result of one AirNow lookup. */
|
|
73
|
+
export interface AirQualityLookup {
|
|
74
|
+
areas: AirQualityArea[];
|
|
75
|
+
/** AirNow's own explanation, verbatim, when it answered HTTP 200 with no data for the location. */
|
|
76
|
+
noDataMessage?: string;
|
|
77
|
+
}
|
|
39
78
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/services/airnow/types.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/services/airnow/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,+EAA+E;AAC/E,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5D,kGAAkG;AAClG,MAAM,WAAW,cAAc;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+BAA+B;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,8EAA8E;AAC9E,MAAM,WAAW,WAAW;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iFAAiF;IACjF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,mFAAmF;AACnF,MAAM,WAAW,UAAU;IACzB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,6EAA6E;IAC7E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,6FAA6F;AAC7F,MAAM,WAAW,cAAc;IAC7B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,UAAU,EAAE,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,mCAAmC;AACnC,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,mGAAmG;IACnG,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview AirNow API response types and normalized domain types
|
|
2
|
+
* @fileoverview AirNow API response types and normalized domain types for the 2026
|
|
3
|
+
* web services: `observation/current/ziplatlong/` and `forecast/current/`.
|
|
3
4
|
* @module services/airnow/types
|
|
4
5
|
*/
|
|
5
6
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/services/airnow/types.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/services/airnow/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|