@jablum/weather-mcp 1.7.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/LICENSE +21 -0
- package/README.md +1319 -0
- package/dist/analytics/anonymizer.d.ts +37 -0
- package/dist/analytics/anonymizer.d.ts.map +1 -0
- package/dist/analytics/anonymizer.js +112 -0
- package/dist/analytics/anonymizer.js.map +1 -0
- package/dist/analytics/collector.d.ts +72 -0
- package/dist/analytics/collector.d.ts.map +1 -0
- package/dist/analytics/collector.js +282 -0
- package/dist/analytics/collector.js.map +1 -0
- package/dist/analytics/config.d.ts +15 -0
- package/dist/analytics/config.d.ts.map +1 -0
- package/dist/analytics/config.js +172 -0
- package/dist/analytics/config.js.map +1 -0
- package/dist/analytics/index.d.ts +8 -0
- package/dist/analytics/index.d.ts.map +1 -0
- package/dist/analytics/index.js +7 -0
- package/dist/analytics/index.js.map +1 -0
- package/dist/analytics/middleware.d.ts +33 -0
- package/dist/analytics/middleware.d.ts.map +1 -0
- package/dist/analytics/middleware.js +99 -0
- package/dist/analytics/middleware.js.map +1 -0
- package/dist/analytics/transport.d.ts +11 -0
- package/dist/analytics/transport.d.ts.map +1 -0
- package/dist/analytics/transport.js +92 -0
- package/dist/analytics/transport.js.map +1 -0
- package/dist/analytics/types.d.ts +74 -0
- package/dist/analytics/types.d.ts.map +1 -0
- package/dist/analytics/types.js +6 -0
- package/dist/analytics/types.js.map +1 -0
- package/dist/config/api.d.ts +30 -0
- package/dist/config/api.d.ts.map +1 -0
- package/dist/config/api.js +32 -0
- package/dist/config/api.js.map +1 -0
- package/dist/config/cache.d.ts +31 -0
- package/dist/config/cache.d.ts.map +1 -0
- package/dist/config/cache.js +108 -0
- package/dist/config/cache.js.map +1 -0
- package/dist/config/displayThresholds.d.ts +83 -0
- package/dist/config/displayThresholds.d.ts.map +1 -0
- package/dist/config/displayThresholds.js +83 -0
- package/dist/config/displayThresholds.js.map +1 -0
- package/dist/config/tools.d.ts +44 -0
- package/dist/config/tools.d.ts.map +1 -0
- package/dist/config/tools.js +269 -0
- package/dist/config/tools.js.map +1 -0
- package/dist/errors/ApiError.d.ts +62 -0
- package/dist/errors/ApiError.d.ts.map +1 -0
- package/dist/errors/ApiError.js +171 -0
- package/dist/errors/ApiError.js.map +1 -0
- package/dist/handlers/airQualityHandler.d.ts +11 -0
- package/dist/handlers/airQualityHandler.d.ts.map +1 -0
- package/dist/handlers/airQualityHandler.js +154 -0
- package/dist/handlers/airQualityHandler.js.map +1 -0
- package/dist/handlers/alertsHandler.d.ts +11 -0
- package/dist/handlers/alertsHandler.d.ts.map +1 -0
- package/dist/handlers/alertsHandler.js +98 -0
- package/dist/handlers/alertsHandler.js.map +1 -0
- package/dist/handlers/currentConditionsHandler.d.ts +13 -0
- package/dist/handlers/currentConditionsHandler.d.ts.map +1 -0
- package/dist/handlers/currentConditionsHandler.js +296 -0
- package/dist/handlers/currentConditionsHandler.js.map +1 -0
- package/dist/handlers/forecastHandler.d.ts +16 -0
- package/dist/handlers/forecastHandler.d.ts.map +1 -0
- package/dist/handlers/forecastHandler.js +454 -0
- package/dist/handlers/forecastHandler.js.map +1 -0
- package/dist/handlers/historicalWeatherHandler.d.ts +12 -0
- package/dist/handlers/historicalWeatherHandler.d.ts.map +1 -0
- package/dist/handlers/historicalWeatherHandler.js +188 -0
- package/dist/handlers/historicalWeatherHandler.js.map +1 -0
- package/dist/handlers/lightningHandler.d.ts +14 -0
- package/dist/handlers/lightningHandler.d.ts.map +1 -0
- package/dist/handlers/lightningHandler.js +258 -0
- package/dist/handlers/lightningHandler.js.map +1 -0
- package/dist/handlers/locationHandler.d.ts +12 -0
- package/dist/handlers/locationHandler.d.ts.map +1 -0
- package/dist/handlers/locationHandler.js +149 -0
- package/dist/handlers/locationHandler.js.map +1 -0
- package/dist/handlers/marineConditionsHandler.d.ts +13 -0
- package/dist/handlers/marineConditionsHandler.d.ts.map +1 -0
- package/dist/handlers/marineConditionsHandler.js +270 -0
- package/dist/handlers/marineConditionsHandler.js.map +1 -0
- package/dist/handlers/riverConditionsHandler.d.ts +11 -0
- package/dist/handlers/riverConditionsHandler.d.ts.map +1 -0
- package/dist/handlers/riverConditionsHandler.js +176 -0
- package/dist/handlers/riverConditionsHandler.js.map +1 -0
- package/dist/handlers/savedLocationsHandler.d.ts +50 -0
- package/dist/handlers/savedLocationsHandler.d.ts.map +1 -0
- package/dist/handlers/savedLocationsHandler.js +397 -0
- package/dist/handlers/savedLocationsHandler.js.map +1 -0
- package/dist/handlers/statusHandler.d.ts +12 -0
- package/dist/handlers/statusHandler.d.ts.map +1 -0
- package/dist/handlers/statusHandler.js +115 -0
- package/dist/handlers/statusHandler.js.map +1 -0
- package/dist/handlers/weatherImageryHandler.d.ts +14 -0
- package/dist/handlers/weatherImageryHandler.d.ts.map +1 -0
- package/dist/handlers/weatherImageryHandler.js +143 -0
- package/dist/handlers/weatherImageryHandler.js.map +1 -0
- package/dist/handlers/wildfireHandler.d.ts +11 -0
- package/dist/handlers/wildfireHandler.d.ts.map +1 -0
- package/dist/handlers/wildfireHandler.js +186 -0
- package/dist/handlers/wildfireHandler.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +735 -0
- package/dist/index.js.map +1 -0
- package/dist/services/blitzortung.d.ts +67 -0
- package/dist/services/blitzortung.d.ts.map +1 -0
- package/dist/services/blitzortung.js +475 -0
- package/dist/services/blitzortung.js.map +1 -0
- package/dist/services/geocoding.d.ts +57 -0
- package/dist/services/geocoding.d.ts.map +1 -0
- package/dist/services/geocoding.js +393 -0
- package/dist/services/geocoding.js.map +1 -0
- package/dist/services/locationStore.d.ts +62 -0
- package/dist/services/locationStore.d.ts.map +1 -0
- package/dist/services/locationStore.js +201 -0
- package/dist/services/locationStore.js.map +1 -0
- package/dist/services/ncei.d.ts +61 -0
- package/dist/services/ncei.d.ts.map +1 -0
- package/dist/services/ncei.js +126 -0
- package/dist/services/ncei.js.map +1 -0
- package/dist/services/nifc.d.ts +44 -0
- package/dist/services/nifc.d.ts.map +1 -0
- package/dist/services/nifc.js +159 -0
- package/dist/services/nifc.js.map +1 -0
- package/dist/services/noaa.d.ts +161 -0
- package/dist/services/noaa.d.ts.map +1 -0
- package/dist/services/noaa.js +681 -0
- package/dist/services/noaa.js.map +1 -0
- package/dist/services/nominatim.d.ts +62 -0
- package/dist/services/nominatim.d.ts.map +1 -0
- package/dist/services/nominatim.js +254 -0
- package/dist/services/nominatim.js.map +1 -0
- package/dist/services/openmeteo.d.ts +189 -0
- package/dist/services/openmeteo.d.ts.map +1 -0
- package/dist/services/openmeteo.js +936 -0
- package/dist/services/openmeteo.js.map +1 -0
- package/dist/services/rainviewer.d.ts +37 -0
- package/dist/services/rainviewer.d.ts.map +1 -0
- package/dist/services/rainviewer.js +115 -0
- package/dist/services/rainviewer.js.map +1 -0
- package/dist/types/imagery.d.ts +82 -0
- package/dist/types/imagery.d.ts.map +1 -0
- package/dist/types/imagery.js +6 -0
- package/dist/types/imagery.js.map +1 -0
- package/dist/types/lightning.d.ts +89 -0
- package/dist/types/lightning.d.ts.map +1 -0
- package/dist/types/lightning.js +6 -0
- package/dist/types/lightning.js.map +1 -0
- package/dist/types/noaa.d.ts +535 -0
- package/dist/types/noaa.d.ts.map +1 -0
- package/dist/types/noaa.js +5 -0
- package/dist/types/noaa.js.map +1 -0
- package/dist/types/nominatim.d.ts +72 -0
- package/dist/types/nominatim.d.ts.map +1 -0
- package/dist/types/nominatim.js +6 -0
- package/dist/types/nominatim.js.map +1 -0
- package/dist/types/openmeteo.d.ts +583 -0
- package/dist/types/openmeteo.d.ts.map +1 -0
- package/dist/types/openmeteo.js +6 -0
- package/dist/types/openmeteo.js.map +1 -0
- package/dist/types/savedLocations.d.ts +58 -0
- package/dist/types/savedLocations.d.ts.map +1 -0
- package/dist/types/savedLocations.js +5 -0
- package/dist/types/savedLocations.js.map +1 -0
- package/dist/types/wildfire.d.ts +83 -0
- package/dist/types/wildfire.d.ts.map +1 -0
- package/dist/types/wildfire.js +5 -0
- package/dist/types/wildfire.js.map +1 -0
- package/dist/utils/airQuality.d.ts +54 -0
- package/dist/utils/airQuality.d.ts.map +1 -0
- package/dist/utils/airQuality.js +251 -0
- package/dist/utils/airQuality.js.map +1 -0
- package/dist/utils/cache.d.ts +69 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +164 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/distance.d.ts +25 -0
- package/dist/utils/distance.d.ts.map +1 -0
- package/dist/utils/distance.js +40 -0
- package/dist/utils/distance.js.map +1 -0
- package/dist/utils/fireWeather.d.ts +76 -0
- package/dist/utils/fireWeather.d.ts.map +1 -0
- package/dist/utils/fireWeather.js +243 -0
- package/dist/utils/fireWeather.js.map +1 -0
- package/dist/utils/geography.d.ts +79 -0
- package/dist/utils/geography.d.ts.map +1 -0
- package/dist/utils/geography.js +266 -0
- package/dist/utils/geography.js.map +1 -0
- package/dist/utils/geohash.d.ts +62 -0
- package/dist/utils/geohash.d.ts.map +1 -0
- package/dist/utils/geohash.js +146 -0
- package/dist/utils/geohash.js.map +1 -0
- package/dist/utils/locationResolver.d.ts +34 -0
- package/dist/utils/locationResolver.d.ts.map +1 -0
- package/dist/utils/locationResolver.js +120 -0
- package/dist/utils/locationResolver.js.map +1 -0
- package/dist/utils/logger.d.ts +75 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +153 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/marine.d.ts +59 -0
- package/dist/utils/marine.d.ts.map +1 -0
- package/dist/utils/marine.js +215 -0
- package/dist/utils/marine.js.map +1 -0
- package/dist/utils/normals.d.ts +86 -0
- package/dist/utils/normals.d.ts.map +1 -0
- package/dist/utils/normals.js +223 -0
- package/dist/utils/normals.js.map +1 -0
- package/dist/utils/snow.d.ts +45 -0
- package/dist/utils/snow.d.ts.map +1 -0
- package/dist/utils/snow.js +144 -0
- package/dist/utils/snow.js.map +1 -0
- package/dist/utils/temperatureConversion.d.ts +12 -0
- package/dist/utils/temperatureConversion.d.ts.map +1 -0
- package/dist/utils/temperatureConversion.js +17 -0
- package/dist/utils/temperatureConversion.js.map +1 -0
- package/dist/utils/timezone.d.ts +56 -0
- package/dist/utils/timezone.d.ts.map +1 -0
- package/dist/utils/timezone.js +167 -0
- package/dist/utils/timezone.js.map +1 -0
- package/dist/utils/units.d.ts +69 -0
- package/dist/utils/units.d.ts.map +1 -0
- package/dist/utils/units.js +158 -0
- package/dist/utils/units.js.map +1 -0
- package/dist/utils/validation.d.ts +89 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +177 -0
- package/dist/utils/validation.js.map +1 -0
- package/dist/utils/version.d.ts +15 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +24 -0
- package/dist/utils/version.js.map +1 -0
- package/package.json +74 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for computing climate normals from historical data
|
|
3
|
+
*
|
|
4
|
+
* Climate normals are 30-year averages (1991-2020) used to provide
|
|
5
|
+
* context for current weather conditions.
|
|
6
|
+
*
|
|
7
|
+
* Hybrid Strategy:
|
|
8
|
+
* 1. Try NCEI (US only, requires token) for official normals
|
|
9
|
+
* 2. Fall back to Open-Meteo (global, no token) computed normals
|
|
10
|
+
*/
|
|
11
|
+
import { celsiusToFahrenheit } from './units.js';
|
|
12
|
+
import { logger } from './logger.js';
|
|
13
|
+
/**
|
|
14
|
+
* Convert millimeters to inches
|
|
15
|
+
*/
|
|
16
|
+
function millimetersToInches(mm) {
|
|
17
|
+
return mm / 25.4;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Compute climate normals from 30 years of historical data
|
|
21
|
+
*
|
|
22
|
+
* @param historicalData - 30 years of daily historical weather data from Open-Meteo
|
|
23
|
+
* @param targetMonth - Month (1-12) to compute normals for
|
|
24
|
+
* @param targetDay - Day of month (1-31) to compute normals for
|
|
25
|
+
* @returns Climate normals (30-year averages) for the specified date
|
|
26
|
+
*/
|
|
27
|
+
export function computeNormalsFrom30YearData(historicalData, targetMonth, targetDay) {
|
|
28
|
+
if (!historicalData.daily || !historicalData.daily.time) {
|
|
29
|
+
throw new Error('Historical data does not contain daily data');
|
|
30
|
+
}
|
|
31
|
+
const daily = historicalData.daily;
|
|
32
|
+
const targetMonthDay = `${String(targetMonth).padStart(2, '0')}-${String(targetDay).padStart(2, '0')}`;
|
|
33
|
+
// Find all occurrences of this month/day across the 30-year period
|
|
34
|
+
const matchingDays = [];
|
|
35
|
+
for (let i = 0; i < daily.time.length; i++) {
|
|
36
|
+
const date = daily.time[i]; // Format: "YYYY-MM-DD"
|
|
37
|
+
const monthDay = date.substring(5); // Extract "MM-DD"
|
|
38
|
+
if (monthDay === targetMonthDay) {
|
|
39
|
+
matchingDays.push({
|
|
40
|
+
tempHigh: daily.temperature_2m_max?.[i],
|
|
41
|
+
tempLow: daily.temperature_2m_min?.[i],
|
|
42
|
+
precipitation: daily.precipitation_sum?.[i]
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (matchingDays.length === 0) {
|
|
47
|
+
throw new Error(`No historical data found for ${targetMonth}/${targetDay}`);
|
|
48
|
+
}
|
|
49
|
+
// Calculate averages
|
|
50
|
+
const tempHighValues = matchingDays.filter(d => d.tempHigh !== undefined).map(d => d.tempHigh);
|
|
51
|
+
const tempLowValues = matchingDays.filter(d => d.tempLow !== undefined).map(d => d.tempLow);
|
|
52
|
+
const precipitationValues = matchingDays.filter(d => d.precipitation !== undefined).map(d => d.precipitation);
|
|
53
|
+
const avgTempHighC = tempHighValues.reduce((sum, val) => sum + val, 0) / tempHighValues.length;
|
|
54
|
+
const avgTempLowC = tempLowValues.reduce((sum, val) => sum + val, 0) / tempLowValues.length;
|
|
55
|
+
const avgPrecipitationMm = precipitationValues.reduce((sum, val) => sum + val, 0) / precipitationValues.length;
|
|
56
|
+
// Convert to US units (Fahrenheit, inches)
|
|
57
|
+
const tempHigh = Math.round(celsiusToFahrenheit(avgTempHighC));
|
|
58
|
+
const tempLow = Math.round(celsiusToFahrenheit(avgTempLowC));
|
|
59
|
+
const precipitation = Math.round(millimetersToInches(avgPrecipitationMm) * 100) / 100; // Round to 2 decimals
|
|
60
|
+
return {
|
|
61
|
+
tempHigh,
|
|
62
|
+
tempLow,
|
|
63
|
+
precipitation,
|
|
64
|
+
source: 'Open-Meteo',
|
|
65
|
+
month: targetMonth,
|
|
66
|
+
day: targetDay
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Generate a cache key for climate normals
|
|
71
|
+
*
|
|
72
|
+
* @param latitude - Latitude (rounded to 2 decimals)
|
|
73
|
+
* @param longitude - Longitude (rounded to 2 decimals)
|
|
74
|
+
* @param month - Month (1-12)
|
|
75
|
+
* @param day - Day of month (1-31)
|
|
76
|
+
* @returns Cache key string
|
|
77
|
+
*/
|
|
78
|
+
export function getNormalsCacheKey(latitude, longitude, month, day) {
|
|
79
|
+
// Round coordinates to 2 decimals to increase cache hit rate
|
|
80
|
+
const lat = Math.round(latitude * 100) / 100;
|
|
81
|
+
const lon = Math.round(longitude * 100) / 100;
|
|
82
|
+
return `normals:${lat}:${lon}:${month}:${day}`;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Calculate departure from normal
|
|
86
|
+
*
|
|
87
|
+
* @param actual - Actual temperature value
|
|
88
|
+
* @param normal - Normal (average) temperature value
|
|
89
|
+
* @returns Departure with sign (e.g., +10, -5)
|
|
90
|
+
*/
|
|
91
|
+
export function calculateDeparture(actual, normal) {
|
|
92
|
+
const departure = Math.round(actual - normal);
|
|
93
|
+
return departure >= 0 ? `+${departure}` : `${departure}`;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Format climate normals for display
|
|
97
|
+
*
|
|
98
|
+
* @param normals - Climate normals data
|
|
99
|
+
* @param currentTemp - Optional current temperature for comparison
|
|
100
|
+
* @returns Formatted markdown string
|
|
101
|
+
*/
|
|
102
|
+
export function formatNormals(normals, currentTemp) {
|
|
103
|
+
let output = `\n## 📊 Climate Context\n\n`;
|
|
104
|
+
output += `**Normal High:** ${normals.tempHigh}°F\n`;
|
|
105
|
+
output += `**Normal Low:** ${normals.tempLow}°F\n`;
|
|
106
|
+
output += `**Normal Precipitation:** ${normals.precipitation}" \n`;
|
|
107
|
+
if (currentTemp) {
|
|
108
|
+
if (currentTemp.high !== undefined) {
|
|
109
|
+
const departure = calculateDeparture(currentTemp.high, normals.tempHigh);
|
|
110
|
+
output += `**High Departure:** ${departure}°F`;
|
|
111
|
+
if (departure.startsWith('+')) {
|
|
112
|
+
output += ` (warmer than normal)`;
|
|
113
|
+
}
|
|
114
|
+
else if (departure.startsWith('-')) {
|
|
115
|
+
output += ` (cooler than normal)`;
|
|
116
|
+
}
|
|
117
|
+
output += `\n`;
|
|
118
|
+
}
|
|
119
|
+
if (currentTemp.low !== undefined) {
|
|
120
|
+
const departure = calculateDeparture(currentTemp.low, normals.tempLow);
|
|
121
|
+
output += `**Low Departure:** ${departure}°F`;
|
|
122
|
+
if (departure.startsWith('+')) {
|
|
123
|
+
output += ` (warmer than normal)`;
|
|
124
|
+
}
|
|
125
|
+
else if (departure.startsWith('-')) {
|
|
126
|
+
output += ` (cooler than normal)`;
|
|
127
|
+
}
|
|
128
|
+
output += `\n`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
output += `\n*Climate normals based on 1991-2020 data*\n`;
|
|
132
|
+
output += `*Source: ${normals.source}*\n`;
|
|
133
|
+
return output;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Get date components from Date object or ISO string
|
|
137
|
+
*
|
|
138
|
+
* @param date - Date object or ISO string
|
|
139
|
+
* @returns Object with month (1-12) and day (1-31)
|
|
140
|
+
*/
|
|
141
|
+
export function getDateComponents(date) {
|
|
142
|
+
const d = typeof date === 'string' ? new Date(date) : date;
|
|
143
|
+
return {
|
|
144
|
+
month: d.getMonth() + 1, // JavaScript months are 0-indexed
|
|
145
|
+
day: d.getDate()
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Check if coordinates are within the contiguous United States
|
|
150
|
+
*
|
|
151
|
+
* @param latitude - Latitude
|
|
152
|
+
* @param longitude - Longitude
|
|
153
|
+
* @returns true if location is in contiguous US
|
|
154
|
+
*/
|
|
155
|
+
export function isLocationInUS(latitude, longitude) {
|
|
156
|
+
// Contiguous US bounding box (approximate)
|
|
157
|
+
// Latitude: ~25°N (Florida) to ~49°N (Canada border)
|
|
158
|
+
// Longitude: ~-125°W (Pacific) to ~-66°W (Atlantic)
|
|
159
|
+
return (latitude >= 24 &&
|
|
160
|
+
latitude <= 50 &&
|
|
161
|
+
longitude >= -125 &&
|
|
162
|
+
longitude <= -66);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Get climate normals using hybrid strategy
|
|
166
|
+
*
|
|
167
|
+
* Strategy:
|
|
168
|
+
* 1. If location is in US and NCEI token available, try NCEI first
|
|
169
|
+
* 2. Fall back to Open-Meteo computed normals (always works)
|
|
170
|
+
*
|
|
171
|
+
* @param openMeteoService - Open-Meteo service instance
|
|
172
|
+
* @param nceiService - NCEI service instance (optional)
|
|
173
|
+
* @param latitude - Latitude
|
|
174
|
+
* @param longitude - Longitude
|
|
175
|
+
* @param month - Month (1-12)
|
|
176
|
+
* @param day - Day of month (1-31)
|
|
177
|
+
* @returns Climate normals with source indication
|
|
178
|
+
*/
|
|
179
|
+
export async function getClimateNormals(openMeteoService, nceiService, latitude, longitude, month, day) {
|
|
180
|
+
// Try NCEI if available and location is in US
|
|
181
|
+
if (nceiService && nceiService.isAvailable() && isLocationInUS(latitude, longitude)) {
|
|
182
|
+
try {
|
|
183
|
+
logger.info('Attempting to fetch climate normals from NCEI', {
|
|
184
|
+
latitude,
|
|
185
|
+
longitude,
|
|
186
|
+
month,
|
|
187
|
+
day
|
|
188
|
+
});
|
|
189
|
+
const normals = await nceiService.getClimateNormals(latitude, longitude, month, day);
|
|
190
|
+
logger.info('Successfully retrieved climate normals from NCEI', {
|
|
191
|
+
latitude,
|
|
192
|
+
longitude,
|
|
193
|
+
month,
|
|
194
|
+
day
|
|
195
|
+
});
|
|
196
|
+
return normals;
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
// NCEI failed, fall back to Open-Meteo
|
|
200
|
+
logger.warn('NCEI climate normals failed, falling back to Open-Meteo', {
|
|
201
|
+
latitude,
|
|
202
|
+
longitude,
|
|
203
|
+
month,
|
|
204
|
+
day,
|
|
205
|
+
error: error.message
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// Use Open-Meteo (default, always works)
|
|
210
|
+
logger.info('Using Open-Meteo for climate normals', {
|
|
211
|
+
latitude,
|
|
212
|
+
longitude,
|
|
213
|
+
month,
|
|
214
|
+
day,
|
|
215
|
+
reason: nceiService && nceiService.isAvailable()
|
|
216
|
+
? 'NCEI fallback'
|
|
217
|
+
: !isLocationInUS(latitude, longitude)
|
|
218
|
+
? 'Location outside US'
|
|
219
|
+
: 'NCEI token not configured'
|
|
220
|
+
});
|
|
221
|
+
return await openMeteoService.getClimateNormals(latitude, longitude, month, day);
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=normals.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normals.js","sourceRoot":"","sources":["../../src/utils/normals.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for extracting and formatting snow data
|
|
3
|
+
* from NOAA API responses
|
|
4
|
+
*/
|
|
5
|
+
import { GridpointProperties, ObservationProperties } from '../types/noaa.js';
|
|
6
|
+
/**
|
|
7
|
+
* Snow data extracted from observations or gridpoint forecasts
|
|
8
|
+
*/
|
|
9
|
+
export interface SnowData {
|
|
10
|
+
snowDepth?: {
|
|
11
|
+
value: number;
|
|
12
|
+
unit: string;
|
|
13
|
+
};
|
|
14
|
+
snowfallAmount?: {
|
|
15
|
+
value: number;
|
|
16
|
+
unit: string;
|
|
17
|
+
period?: string;
|
|
18
|
+
};
|
|
19
|
+
iceAccumulation?: {
|
|
20
|
+
value: number;
|
|
21
|
+
unit: string;
|
|
22
|
+
period?: string;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Extract current snow depth from observation data
|
|
27
|
+
*/
|
|
28
|
+
export declare function extractSnowDepth(observation: ObservationProperties): SnowData['snowDepth'] | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Extract snowfall forecast from gridpoint data for a specific time period
|
|
31
|
+
*/
|
|
32
|
+
export declare function extractSnowfallForecast(gridpoint: GridpointProperties, startTime?: Date, endTime?: Date): SnowData['snowfallAmount'] | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Extract ice accumulation forecast from gridpoint data
|
|
35
|
+
*/
|
|
36
|
+
export declare function extractIceAccumulation(gridpoint: GridpointProperties, startTime?: Date, endTime?: Date): SnowData['iceAccumulation'] | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Format snow data for display
|
|
39
|
+
*/
|
|
40
|
+
export declare function formatSnowData(snowData: SnowData): string;
|
|
41
|
+
/**
|
|
42
|
+
* Check if there's any significant winter weather to report
|
|
43
|
+
*/
|
|
44
|
+
export declare function hasWinterWeather(snowData: SnowData): boolean;
|
|
45
|
+
//# sourceMappingURL=snow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snow.d.ts","sourceRoot":"","sources":["../../src/utils/snow.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAE9E;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,SAAS,CAAC,EAAE;QACV,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,cAAc,CAAC,EAAE;QACf,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,eAAe,CAAC,EAAE;QAChB,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;CACH;AASD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,qBAAqB,GAAG,QAAQ,CAAC,WAAW,CAAC,GAAG,SAAS,CA8BtG;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,mBAAmB,EAC9B,SAAS,CAAC,EAAE,IAAI,EAChB,OAAO,CAAC,EAAE,IAAI,GACb,QAAQ,CAAC,gBAAgB,CAAC,GAAG,SAAS,CA2CxC;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,mBAAmB,EAC9B,SAAS,CAAC,EAAE,IAAI,EAChB,OAAO,CAAC,EAAE,IAAI,GACb,QAAQ,CAAC,iBAAiB,CAAC,GAAG,SAAS,CAyCzC;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAkBzD;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAE5D"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for extracting and formatting snow data
|
|
3
|
+
* from NOAA API responses
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Convert millimeters to inches
|
|
7
|
+
*/
|
|
8
|
+
function mmToInches(mm) {
|
|
9
|
+
return mm / 25.4;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Extract current snow depth from observation data
|
|
13
|
+
*/
|
|
14
|
+
export function extractSnowDepth(observation) {
|
|
15
|
+
if (!observation.snowDepth || observation.snowDepth.value === null) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
// NOAA returns snow depth in various units, but typically centimeters or meters
|
|
19
|
+
const value = observation.snowDepth.value;
|
|
20
|
+
const unit = observation.snowDepth.unitCode;
|
|
21
|
+
let inches;
|
|
22
|
+
if (unit?.includes('cm') || unit?.includes('centimeter')) {
|
|
23
|
+
inches = value / 2.54; // cm to inches
|
|
24
|
+
}
|
|
25
|
+
else if (unit?.includes('m') && !unit?.includes('mm')) {
|
|
26
|
+
inches = value * 39.3701; // meters to inches
|
|
27
|
+
}
|
|
28
|
+
else if (unit?.includes('mm') || unit?.includes('millimeter')) {
|
|
29
|
+
inches = mmToInches(value);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
// Assume centimeters if unit unclear
|
|
33
|
+
inches = value / 2.54;
|
|
34
|
+
}
|
|
35
|
+
// Only return if there's actually snow on the ground
|
|
36
|
+
if (inches < 0.1) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
value: Math.round(inches * 10) / 10, // Round to 1 decimal
|
|
41
|
+
unit: 'in'
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Extract snowfall forecast from gridpoint data for a specific time period
|
|
46
|
+
*/
|
|
47
|
+
export function extractSnowfallForecast(gridpoint, startTime, endTime) {
|
|
48
|
+
if (!gridpoint.snowfallAmount || gridpoint.snowfallAmount.values.length === 0) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
let totalSnowfall = 0;
|
|
52
|
+
const snowfallSeries = gridpoint.snowfallAmount;
|
|
53
|
+
for (const entry of snowfallSeries.values) {
|
|
54
|
+
if (entry.value === null || entry.value === undefined) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
// Parse the time interval (e.g., "2025-11-07T12:00:00+00:00/PT6H")
|
|
58
|
+
const [validTimeStr] = entry.validTime.split('/');
|
|
59
|
+
const validTime = new Date(validTimeStr);
|
|
60
|
+
// If we have a time range filter, check if this entry is within it
|
|
61
|
+
if (startTime && validTime < startTime) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (endTime && validTime > endTime) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
totalSnowfall += entry.value;
|
|
68
|
+
}
|
|
69
|
+
// NOAA returns snowfall in millimeters
|
|
70
|
+
const inches = mmToInches(totalSnowfall);
|
|
71
|
+
// Only return if there's meaningful snowfall
|
|
72
|
+
if (inches < 0.1) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
value: Math.round(inches * 10) / 10, // Round to 1 decimal
|
|
77
|
+
unit: 'in',
|
|
78
|
+
period: startTime && endTime ?
|
|
79
|
+
`${startTime.toLocaleDateString()} - ${endTime.toLocaleDateString()}` :
|
|
80
|
+
undefined
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Extract ice accumulation forecast from gridpoint data
|
|
85
|
+
*/
|
|
86
|
+
export function extractIceAccumulation(gridpoint, startTime, endTime) {
|
|
87
|
+
if (!gridpoint.iceAccumulation || gridpoint.iceAccumulation.values.length === 0) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
let totalIce = 0;
|
|
91
|
+
const iceSeries = gridpoint.iceAccumulation;
|
|
92
|
+
for (const entry of iceSeries.values) {
|
|
93
|
+
if (entry.value === null || entry.value === undefined) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const [validTimeStr] = entry.validTime.split('/');
|
|
97
|
+
const validTime = new Date(validTimeStr);
|
|
98
|
+
if (startTime && validTime < startTime) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (endTime && validTime > endTime) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
totalIce += entry.value;
|
|
105
|
+
}
|
|
106
|
+
// NOAA returns ice accumulation in millimeters
|
|
107
|
+
const inches = mmToInches(totalIce);
|
|
108
|
+
// Only return if there's meaningful ice accumulation
|
|
109
|
+
if (inches < 0.05) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
value: Math.round(inches * 100) / 100, // Round to 2 decimals (ice is often small amounts)
|
|
114
|
+
unit: 'in',
|
|
115
|
+
period: startTime && endTime ?
|
|
116
|
+
`${startTime.toLocaleDateString()} - ${endTime.toLocaleDateString()}` :
|
|
117
|
+
undefined
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Format snow data for display
|
|
122
|
+
*/
|
|
123
|
+
export function formatSnowData(snowData) {
|
|
124
|
+
const parts = [];
|
|
125
|
+
if (snowData.snowDepth) {
|
|
126
|
+
parts.push(`**Snow Depth:** ${snowData.snowDepth.value}${snowData.snowDepth.unit} on ground`);
|
|
127
|
+
}
|
|
128
|
+
if (snowData.snowfallAmount) {
|
|
129
|
+
const period = snowData.snowfallAmount.period ? ` (${snowData.snowfallAmount.period})` : '';
|
|
130
|
+
parts.push(`**Snowfall Forecast:** ${snowData.snowfallAmount.value}${snowData.snowfallAmount.unit}${period}`);
|
|
131
|
+
}
|
|
132
|
+
if (snowData.iceAccumulation) {
|
|
133
|
+
const period = snowData.iceAccumulation.period ? ` (${snowData.iceAccumulation.period})` : '';
|
|
134
|
+
parts.push(`**Ice Accumulation:** ${snowData.iceAccumulation.value}${snowData.iceAccumulation.unit}${period}`);
|
|
135
|
+
}
|
|
136
|
+
return parts.length > 0 ? `\n## ❄️ Winter Weather\n\n${parts.join('\n')}\n` : '';
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Check if there's any significant winter weather to report
|
|
140
|
+
*/
|
|
141
|
+
export function hasWinterWeather(snowData) {
|
|
142
|
+
return !!(snowData.snowDepth || snowData.snowfallAmount || snowData.iceAccumulation);
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=snow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snow.js","sourceRoot":"","sources":["../../src/utils/snow.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temperature conversion utilities
|
|
3
|
+
* Centralized to avoid duplication across handlers
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Convert temperature value to Fahrenheit based on unit code
|
|
7
|
+
* @param value - Temperature value (can be null)
|
|
8
|
+
* @param unitCode - Unit code from NOAA API (e.g., "wmoUnit:degC", "wmoUnit:degF")
|
|
9
|
+
* @returns Temperature in Fahrenheit, or null if value is null
|
|
10
|
+
*/
|
|
11
|
+
export declare function convertToFahrenheit(value: number | null, unitCode: string): number | null;
|
|
12
|
+
//# sourceMappingURL=temperatureConversion.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"temperatureConversion.d.ts","sourceRoot":"","sources":["../../src/utils/temperatureConversion.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGzF"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temperature conversion utilities
|
|
3
|
+
* Centralized to avoid duplication across handlers
|
|
4
|
+
*/
|
|
5
|
+
import { celsiusToFahrenheit } from './units.js';
|
|
6
|
+
/**
|
|
7
|
+
* Convert temperature value to Fahrenheit based on unit code
|
|
8
|
+
* @param value - Temperature value (can be null)
|
|
9
|
+
* @param unitCode - Unit code from NOAA API (e.g., "wmoUnit:degC", "wmoUnit:degF")
|
|
10
|
+
* @returns Temperature in Fahrenheit, or null if value is null
|
|
11
|
+
*/
|
|
12
|
+
export function convertToFahrenheit(value, unitCode) {
|
|
13
|
+
if (value === null)
|
|
14
|
+
return null;
|
|
15
|
+
return unitCode.includes('degC') ? celsiusToFahrenheit(value) : value;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=temperatureConversion.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"temperatureConversion.js","sourceRoot":"","sources":["../../src/utils/temperatureConversion.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEjD;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAoB,EAAE,QAAgB;IACxE,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAChC,OAAO,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;AACxE,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for timezone-aware time formatting
|
|
3
|
+
* Uses Luxon for timezone support with native Intl API
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Format an ISO 8601 datetime string in the specified timezone
|
|
7
|
+
* @param isoString - ISO 8601 datetime string (e.g., "2025-11-07T14:30:00+00:00")
|
|
8
|
+
* @param timezone - IANA timezone identifier (e.g., "America/New_York", "Asia/Tokyo")
|
|
9
|
+
* @param format - Optional format style ('full', 'long', 'medium', 'short')
|
|
10
|
+
* @returns Formatted datetime string in local timezone
|
|
11
|
+
*/
|
|
12
|
+
export declare function formatInTimezone(isoString: string, timezone: string, format?: 'full' | 'long' | 'medium' | 'short'): string;
|
|
13
|
+
/**
|
|
14
|
+
* Format a date-only ISO string in the specified timezone
|
|
15
|
+
* @param isoString - ISO 8601 datetime string
|
|
16
|
+
* @param timezone - IANA timezone identifier
|
|
17
|
+
* @returns Formatted date string (e.g., "Nov 7, 2025")
|
|
18
|
+
*/
|
|
19
|
+
export declare function formatDateInTimezone(isoString: string, timezone: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Format a time-only ISO string in the specified timezone
|
|
22
|
+
* @param isoString - ISO 8601 datetime string
|
|
23
|
+
* @param timezone - IANA timezone identifier
|
|
24
|
+
* @returns Formatted time string (e.g., "2:30 PM EST")
|
|
25
|
+
*/
|
|
26
|
+
export declare function formatTimeInTimezone(isoString: string, timezone: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Get timezone abbreviation from IANA timezone identifier
|
|
29
|
+
* @param timezone - IANA timezone identifier (e.g., "America/New_York")
|
|
30
|
+
* @param datetime - Optional datetime to determine DST status
|
|
31
|
+
* @returns Timezone abbreviation (e.g., "EST" or "EDT")
|
|
32
|
+
*/
|
|
33
|
+
export declare function getTimezoneAbbreviation(timezone: string, datetime?: Date): string;
|
|
34
|
+
/**
|
|
35
|
+
* Detect timezone from coordinates using a simple heuristic
|
|
36
|
+
* This is a fallback when timezone is not provided by APIs
|
|
37
|
+
* @param latitude - Latitude
|
|
38
|
+
* @param longitude - Longitude
|
|
39
|
+
* @returns Best-guess IANA timezone identifier
|
|
40
|
+
*/
|
|
41
|
+
export declare function guessTimezoneFromCoords(latitude: number, longitude: number): string;
|
|
42
|
+
/**
|
|
43
|
+
* Format a time range with timezone context
|
|
44
|
+
* @param startTime - Start time ISO string
|
|
45
|
+
* @param endTime - End time ISO string
|
|
46
|
+
* @param timezone - IANA timezone identifier
|
|
47
|
+
* @returns Formatted range (e.g., "Nov 7, 2:00 PM - 5:00 PM EST")
|
|
48
|
+
*/
|
|
49
|
+
export declare function formatTimeRangeInTimezone(startTime: string, endTime: string, timezone: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Check if a timezone is valid IANA timezone identifier
|
|
52
|
+
* @param timezone - Timezone string to validate
|
|
53
|
+
* @returns true if valid, false otherwise
|
|
54
|
+
*/
|
|
55
|
+
export declare function isValidTimezone(timezone: string): boolean;
|
|
56
|
+
//# sourceMappingURL=timezone.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timezone.d.ts","sourceRoot":"","sources":["../../src/utils/timezone.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,MAAM,GAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAkB,GACtD,MAAM,CA2BR;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAahF;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAmBhF;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,IAAI,GAAG,MAAM,CAUjF;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAkBnF;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,MAAM,CAmBR;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAOzD"}
|