Back to APIs list

Weather API examples & templates

Use these vals as a playground to view and fork Weather API examples and templates on Val Town. Run any example below or find templates that can be used as a pre-built solution.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
const API_KEY = Deno.env.get("OPENWEATHERMAP_APP_ID");
const LATITUDE = Deno.env.get("LATITUDE");
const LONGITUDE = Deno.env.get("LONGITUDE");
const CHAT_ID = Deno.env.get("TELEGRAM_CHAT_ID");
const TELEGRAM_TOKEN = Deno.env.get("TELEGRAM_TOKEN");
const THINGSPEAK_CHANNEL_ID = Deno.env.get("THINGSPEAK_CHANNEL_ID");
const fetchLatestTemperatureFromThingSpeak = async (channelId = THINGSPEAK_CHANNEL_ID, resultsCount = 1) => {
const baseUrl = "https://api.thingspeak.com/channels";
const url = `${baseUrl}/${channelId}/feeds.json?results=${resultsCount}`;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const temperature = data.feeds[0]?.field3;
const createdAt = new Date(data.feeds[0]?.created_at);
const currentTime = new Date();
const oneHourAgo = new Date(currentTime.getTime() - (60 * 60 * 1000));
if (temperature === undefined || createdAt < oneHourAgo) {
throw new Error("Temperature data not found in the feed.");
}
return temperature;
} catch (error) {
// console.error("Failed to fetch temperature:", error);
return null; // or handle the error in another way
}
};
const weatherToEmoticon = {
"clear sky": "☀️",
"few clouds": "🌤",
"scattered clouds": "⛅",
"broken clouds": "☁️",
"shower rain": "🌧",
"rain": "🌧",
"thunderstorm": "⛈",
"snow": "❄️",
"mist": "🌫",
};
const fetchDataFromOpenWeatherMap = async () => {
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?lat=${LATITUDE}&lon=${LONGITUDE}&units=metric&appid=${API_KEY}`,
);
const weatherForecastData = await response.json();
const condition = weatherForecastData.weather[0].icon;
// const weatherIcon = fetch(`https://openweathermap.org/img/wn/${condition}@2x.png`); // https://openweathermap.org/img/wn/10d@2x.png
const weatherCondition = weatherForecastData.weather[0].description;
const temperature = weatherForecastData.main.temp;
const humidity = weatherForecastData.main.humidity;
const wind = weatherForecastData.wind;
return {
temperature,
humidity,
weatherCondition,
wind,
};
};
const sendTextWithTelegram = async (text: string) => {
const telegramSendUrl =
`https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage?chat_id=${CHAT_ID}&parse_mode=HTML&text=${text}`;
await fetch(telegramSendUrl);
};
export default async function(interval: Interval) {
const temperature = await fetchLatestTemperatureFromThingSpeak();
const weatherCondition = await fetchDataFromOpenWeatherMap();
const windspeedInKmh = Math.ceil(weatherCondition.wind.speed * 3.6);
const formattedWeatherData = `<b>Temperature:</b> <i>${temperature || weatherCondition.temperature} °C</i>%0A
<b>Humidity:</b> <i>${weatherCondition.humidity}%</i>%0A
<b>Weather:</b> <i>${weatherCondition.weatherCondition}</i>%0A
<b>Wind Speed:</b> <i>${windspeedInKmh} km/h</i>%0A
<b>Wind Direction:</b> <i>${weatherCondition.wind.deg}°</i>%0A${
!temperature ? `<b>Status:</b> On-site sensor OFFLINE%0A` : ""
}`;
sendTextWithTelegram(
formattedWeatherData,
);
}

Get Weather

Simple function to get weather data from the free wttr.in service.

Create valimport { getWeather } from "https://esm.town/v/stevekrouse/getWeather"; let weather = await getWeather("Brooklyn, NY"); console.log(weather.current_condition[0].FeelsLikeF)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { fetchJSON } from "https://esm.town/v/stevekrouse/fetchJSON";
export async function getWeather(location: string): Promise<WeatherResponse> {
return fetchJSON(`https://wttr.in/${location}?format=j1`);
}
export interface WeatherResponse {
current_condition: CurrentCondition[];
nearest_area: NearestArea[];
request: Request[];
weather: Weather[];
}
export interface CurrentCondition {
FeelsLikeC: string;
FeelsLikeF: string;
cloudcover: string;
humidity: string;
localObsDateTime: string;
observation_time: string;
precipInches: string;
precipMM: string;
pressure: string;
pressureInches: string;
temp_C: string;
temp_F: string;
uvIndex: string;
visibility: string;
visibilityMiles: string;
weatherCode: string;
weatherDesc: WeatherDesc[];
weatherIconUrl: WeatherDesc[];
winddir16Point: string;
winddirDegree: string;
windspeedKmph: string;
windspeedMiles: string;
}
export interface WeatherDesc {
value: string;
}
export interface NearestArea {
areaName: WeatherDesc[];
country: WeatherDesc[];
latitude: string;
longitude: string;
population: string;
region: WeatherDesc[];
weatherUrl: WeatherDesc[];
}
export interface Request {
query: string;
type: string;
}
export interface Weather {
astronomy: Astronomy[];
avgtempC: string;
avgtempF: string;
date: Date;
hourly: Hourly[];
maxtempC: string;
maxtempF: string;
mintempC: string;
mintempF: string;
sunHour: string;
totalSnow_cm: string;
uvIndex: string;
}
export interface Astronomy {
moon_illumination: string;
moon_phase: string;
moonrise: string;
moonset: string;
sunrise: string;
sunset: string;
}
export interface Hourly {
DewPointC: string;
DewPointF: string;
FeelsLikeC: string;
FeelsLikeF: string;
HeatIndexC: string;
HeatIndexF: string;
WindChillC: string;
WindChillF: string;
WindGustKmph: string;
WindGustMiles: string;
chanceoffog: string;
chanceoffrost: string;
chanceofhightemp: string;
chanceofovercast: string;
chanceofrain: string;
chanceofremdry: string;
chanceofsnow: string;
chanceofsunshine: string;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { fetchJSON } from "https://esm.town/v/stevekrouse/fetchJSON";
export async function getWeather(city: string): Promise<WeatherResponse> {
return fetchJSON(`https://wttr.in/${city}?format=j1`);
}
export interface WeatherResponse {
current_condition: CurrentCondition[];
nearest_area: NearestArea[];
request: Request[];
weather: Weather[];
}
export interface CurrentCondition {
FeelsLikeC: string;
FeelsLikeF: string;
cloudcover: string;
humidity: string;
localObsDateTime: string;
observation_time: string;
precipInches: string;
precipMM: string;
pressure: string;
pressureInches: string;
temp_C: string;
temp_F: string;
uvIndex: string;
visibility: string;
visibilityMiles: string;
weatherCode: string;
weatherDesc: WeatherDesc[];
weatherIconUrl: WeatherDesc[];
winddir16Point: string;
winddirDegree: string;
windspeedKmph: string;
windspeedMiles: string;
}
export interface WeatherDesc {
value: string;
}
export interface NearestArea {
areaName: WeatherDesc[];
country: WeatherDesc[];
latitude: string;
longitude: string;
population: string;
region: WeatherDesc[];
weatherUrl: WeatherDesc[];
}
export interface Request {
query: string;
type: string;
}
export interface Weather {
astronomy: Astronomy[];
avgtempC: string;
avgtempF: string;
date: Date;
hourly: Hourly[];
maxtempC: string;
maxtempF: string;
mintempC: string;
mintempF: string;
sunHour: string;
totalSnow_cm: string;
uvIndex: string;
}
export interface Astronomy {
moon_illumination: string;
moon_phase: string;
moonrise: string;
moonset: string;
sunrise: string;
sunset: string;
}
export interface Hourly {
DewPointC: string;
DewPointF: string;
FeelsLikeC: string;
FeelsLikeF: string;
HeatIndexC: string;
HeatIndexF: string;
WindChillC: string;
WindChillF: string;
WindGustKmph: string;
WindGustMiles: string;
chanceoffog: string;
chanceoffrost: string;
chanceofhightemp: string;
chanceofovercast: string;
chanceofrain: string;
chanceofremdry: string;
chanceofsnow: string;
chanceofsunshine: string;

☔️ Umbrella reminder if there's rain today

Screenshot 2023-09-14 at 12.31.32.png

Setup

  1. Fork this val 👉 https://val.town/v/stevekrouse.umbrellaReminder/fork
  2. Customize the location (line 8). You can supply any free-form description of a location.

⚠️ Only works for US-based locations (where weather.gov covers).

How it works

  1. Geocodes an free-form description of a location to latitude and longitude – @stevekrouse.nominatimSearch
  2. Converts a latitude and longitude to weather.gov grid – @stevekrouse.weatherGovGrid
  3. Gets the hourly forecast for that grid
  4. Filters the forecast for periods that are today and >30% chance of rain
  5. If there are any, it formats them appropriately, and sends me an email
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { email } from "https://esm.town/v/std/email?v=9";
import { fetchJSON } from "https://esm.town/v/stevekrouse/fetchJSON";
import { nominatimSearch } from "https://esm.town/v/stevekrouse/nominatimSearch";
import { weatherGovGrid } from "https://esm.town/v/stevekrouse/weatherGovGrid";
export const umbrellaReminder = async (arg) => {
if (arg.method) return Response.json("");
let location = "radford, virginia"; // <---- customize this line
let [{ lat, lon }] = await nominatimSearch({
q: location,
});
let { properties: grid } = await weatherGovGrid({
lat,
lon,
});
let { properties: { periods } } = await fetchJSON(
grid.forecastHourly,
);
let { DateTime } = await import("npm:luxon");
let parse = (iso) => DateTime.fromISO(iso).setZone(grid.timeZone);
let today = periods.filter((x) =>
parse(x.startTime).toLocaleString()
=== DateTime.now().setZone(grid.timeZone).toLocaleString()
);
if (today.every((x) => x.probabilityOfPrecipitation.value < 30))
return today;
let format = (iso) => parse(iso).toFormat("ha").toLowerCase();
let html = `The probabilities of rain in <b>${location}</b> today:<br><br>`
+ today.map((
{ startTime, endTime, probabilityOfPrecipitation: { value: p } },
) => `${format(startTime)}-${format(endTime)}: ${p}%`).join("<br>");
return email({ html, subject: "🧥 Carry an umbrella today!" });
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { email } from "https://esm.town/v/std/email?v=12";
import { getWeather } from "https://esm.town/v/stevekrouse/getWeather?v=2";
export default async function(interval: Interval) {
let weather = await getWeather("Brooklyn, NY");
let temperature = weather.current_condition[0].FeelsLikeF;
let jacket;
if (temperature > 0 && temperature <= 45) {
jacket = "big coat";
} else if (temperature > 45 && temperature <= 60) {
jacket = "light coat";
} else if (temperature > 60 && temperature <= 70) {
jacket = "light jacket";
} else if (temperature > 70 && temperature < 76) {
jacket = "cardigan";
} else if (temperature >= 76) {
jacket = "none";
}
console.log(`Jacket: ${jacket}`);
email({
subject: `Jacket Needed: ${jacket} (${temperature}°F)`,
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
const TELEGRAM_CHAT_ID = Deno.env.get("TELEGRAM_CHAT_ID");
const TELEGRAM_TOKEN = Deno.env.get("TELEGRAM_TOKEN");
const latitude = Deno.env.get("LATITUDE");
const longitude = Deno.env.get("LONGITUDE");
const openWeatherMapAppId = Deno.env.get("OPENWEATHERMAP_APP_ID");
type WeatherData = {
date: string;
minTemperature: string;
maxTemperature: string;
willItRain: string;
willItSnow: string;
totalRain: string;
};
type Forecast = {
today: WeatherData;
tomorrow: WeatherData;
dayAfterTomorrow: WeatherData;
};
const formatWeatherForecast = (forecast: Forecast): string => {
const formatDate = (data: WeatherData) => `
<b>Date:</b> ${data.date}%0A
<b>Min Temperature:</b> <i>${data.minTemperature} °C</i>%0A
<b>Max Temperature:</b> <i>${data.maxTemperature} °C</i>%0A
${data.willItRain === "Yes" ? `<b>Rain:</b> ${data.willItRain} (${data.totalRain})%0A` : ""}
${data.willItSnow === "Yes" ? `<b>Snow:</b> ${data.willItSnow}%0A` : ""}`;
return `
<b>Weather Forecast</b>🌤️%0A
<u>Today:</u>%0A
${formatDate(forecast.today)}
<u>Tomorrow:</u>%0A
${formatDate(forecast.tomorrow)}
<u>Day After Tomorrow:</u>%0A
${formatDate(forecast.dayAfterTomorrow)}
`;
};
const extractForecast = (weatherData, date) => {
const forecasts = weatherData.list.filter(entry => entry.dt_txt.startsWith(date));
let minTemp = forecasts[0].main.temp_min;
let maxTemp = forecasts[0].main.temp_max;
let willRain = false;
let willSnow = false;
let totalRain = 0; // Initialize total rain
// Iterate over the forecasts to find min, max temperatures and rain/snow status
forecasts.forEach(entry => {
if (entry.main.temp_min < minTemp) minTemp = entry.main.temp_min;
if (entry.main.temp_max > maxTemp) maxTemp = entry.main.temp_max;
if (entry.weather.some(w => w.main.toLowerCase() === "rain")) {
willRain = true;
if (entry.rain && entry.rain["3h"]) {
totalRain += entry.rain["3h"]; // Add the rain volume from this forecast to the total
}
}
if (entry.weather.some(w => w.main.toLowerCase() === "snow")) {
willSnow = true;
}
});
const result = {
date,
minTemperature: minTemp.toFixed(2),
maxTemperature: maxTemp.toFixed(2),
willItRain: willRain ? "Yes" : "No",
willItSnow: willSnow ? "Yes" : "No",
totalRain: willRain ? totalRain.toFixed(2) + " mm" : "0 mm", // Always include totalRain, set to "0 mm" if no rain
};
return result;
};
const fetchForecast = async () => {
const response = await fetch(
`https://api.openweathermap.org/data/2.5/forecast?lat=${latitude}&lon=${longitude}&units=metric&appid=${openWeatherMapAppId}`,
);
const data = await response.json();
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(today.getDate() + 1);
const dayAfterTomorrow = new Date(today);
dayAfterTomorrow.setDate(today.getDate() + 2);
const todayForecast = extractForecast(data, today.toISOString().slice(0, 10));
const tomorrowForecast = extractForecast(data, tomorrow.toISOString().slice(0, 10));
const dayAfterTomorrowForecast = extractForecast(data, dayAfterTomorrow.toISOString().slice(0, 10));
const forecastString = {
today: todayForecast,
tomorrow: tomorrowForecast,
dayAfterTomorrow: dayAfterTomorrowForecast,
};
return forecastString;

☔️ Umbrella reminder if there's rain today

Screenshot 2023-09-14 at 12.31.32.png

Setup

  1. Fork this val 👉 https://val.town/v/stevekrouse.umbrellaReminder/fork
  2. Customize the location (line 8). You can supply any free-form description of a location.

⚠️ Only works for US-based locations (where weather.gov covers).

How it works

  1. Geocodes an free-form description of a location to latitude and longitude – @stevekrouse.nominatimSearch
  2. Converts a latitude and longitude to weather.gov grid – @stevekrouse.weatherGovGrid
  3. Gets the hourly forecast for that grid
  4. Filters the forecast for periods that are today and >30% chance of rain
  5. If there are any, it formats them appropriately, and sends me an email
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { email } from "https://esm.town/v/std/email?v=9";
import { fetchJSON } from "https://esm.town/v/stevekrouse/fetchJSON";
import { nominatimSearch } from "https://esm.town/v/stevekrouse/nominatimSearch";
import { weatherGovGrid } from "https://esm.town/v/stevekrouse/weatherGovGrid";
export const umbrellaReminder = async (arg) => {
if (arg.method) return Response.json("");
let location = "Little Elm, Texas"; // <---- customize this line
let [{ lat, lon }] = await nominatimSearch({
q: location,
});
let { properties: grid } = await weatherGovGrid({
lat,
lon,
});
let { properties: { periods } } = await fetchJSON(
grid.forecastHourly,
);
let { DateTime } = await import("npm:luxon");
let parse = (iso) => DateTime.fromISO(iso).setZone(grid.timeZone);
let today = periods.filter((x) =>
parse(x.startTime).toLocaleString()
=== DateTime.now().setZone(grid.timeZone).toLocaleString()
);
if (today.every((x) => x.probabilityOfPrecipitation.value < 10))
return email({
html: `Enjoy the dry weather in <b>${location}</b> today!`,
subject: "☀️ No need for an umbrella today!",
});
let format = (iso) => parse(iso).toFormat("ha").toLowerCase();
let html = `The probabilities of rain in <b>${location}</b> today:<br><br>`
+ today.map((
{ startTime, endTime, probabilityOfPrecipitation: { value: p } },
) => `${format(startTime)}-${format(endTime)}: ${p}%`).join("<br>");
return email({ html, subject: "☔️ Carry an umbrella today!" });
};

☔️ Umbrella reminder if there's rain today

Screenshot 2023-09-14 at 12.31.32.png

Setup

  1. Fork this val 👉 https://val.town/v/stevekrouse.umbrellaReminder/fork
  2. Customize the location (line 8). You can supply any free-form description of a location.

⚠️ Only works for US-based locations (where weather.gov covers).

How it works

  1. Geocodes an free-form description of a location to latitude and longitude – @stevekrouse.nominatimSearch
  2. Converts a latitude and longitude to weather.gov grid – @stevekrouse.weatherGovGrid
  3. Gets the hourly forecast for that grid
  4. Filters the forecast for periods that are today and >30% chance of rain
  5. If there are any, it formats them appropriately, and sends me an email
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { email } from "https://esm.town/v/std/email?v=9";
import { fetchJSON } from "https://esm.town/v/stevekrouse/fetchJSON";
import { nominatimSearch } from "https://esm.town/v/stevekrouse/nominatimSearch";
import { weatherGovGrid } from "https://esm.town/v/stevekrouse/weatherGovGrid";
export const umbrellaReminder = async (arg) => {
if (arg.method) return Response.json("");
let location = "prospect heights, brooklyn"; // <---- customize this line
let [{ lat, lon }] = await nominatimSearch({
q: location,
});
let { properties: grid } = await weatherGovGrid({
lat,
lon,
});
let { properties: { periods } } = await fetchJSON(
grid.forecastHourly,
);
let { DateTime } = await import("npm:luxon");
let parse = (iso) => DateTime.fromISO(iso).setZone(grid.timeZone);
let today = periods.filter((x) =>
parse(x.startTime).toLocaleString()
=== DateTime.now().setZone(grid.timeZone).toLocaleString()
);
if (today.every((x) => x.probabilityOfPrecipitation.value < 30))
return today;
let format = (iso) => parse(iso).toFormat("ha").toLowerCase();
let html = `The probabilities of rain in <b>${location}</b> today:<br><br>`
+ today.map((
{ startTime, endTime, probabilityOfPrecipitation: { value: p } },
) => `${format(startTime)}-${format(endTime)}: ${p}%`).join("<br>");
return email({ html, subject: "☔️ Carry an umbrella today!" });
};

Exponential backoff middleware

If your server returns a 5xx error, it will wait 1, 2, 4, 8, 16, 32, 64, 128... seconds before retrying

Screenshot 2024-04-26 at 18.07.40.gif

Usage

Create valimport { exponentialBackoffMiddleware } from "https://esm.town/v/stevekrouse/exponentialBackoffMiddleware" export default exponentialBackoffMiddleware(() => { /* your normal http handler * / })

Example usage: https://www.val.town/v/stevekrouse/BIGweather?v=164#L114

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/** @jsxImportSource https://esm.sh/preact */
import { html } from "https://esm.town/v/stevekrouse/html";
import { render } from "npm:preact-render-to-string";
/**
* Exponential backoff middleware
* If your server returns a 5xx error,
* it will wait 1, 2, 4, 8, 16, 32, 64, 128 seconds before retrying
* @param http handler
*/
export function exponentialBackoffMiddleware(
handler: (req: Request) => Response | Promise<Response>,
): (req: Request) => Promise<Response> {
return async (req: Request): Promise<Response> => {
const res = await handler(req);
if (res.status < 500) {
return res;
}
else {
const url = new URL(req.url);
const retryAfter = Number(url.searchParams.get("retryAfter")) || 1;
const nextRetryAfter = Math.min(retryAfter * 2, 128);
const nextURL = new URL(req.url);
nextURL.searchParams.set("retryAfter", nextRetryAfter.toString());
return html(render(
<html>
<head>
<meta http-equiv="refresh" content={retryAfter + "; url=" + nextURL} />
<title>{res.status} Error</title>
</head>
<body>
<h1>{res.status} Error</h1>
<p>
{res.statusText}
</p>
<p>Will retry in {retryAfter} seconds</p>
</body>
</html>,
));
}
};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { OpenAI } from "https://esm.town/v/std/openai?v=2";
const openai = new OpenAI();
const functionExpression = await openai.chat.completions.create({
"messages": [
{ "role": "user", "content": "whats the weather in sf" },
],
tools: [
{
function: {
name: "weather",
parameters: {
"type": "object",
"properties": {
"location": { "type": "string", "description": "The city and state e.g. San Francisco, CA" },
"unit": { "type": "string", "enum": ["c", "f"] },
},
"required": ["location"],
},
},
type: "function",
},
],
model: "gpt-4",
});
console.log(functionExpression.choices[0].message.tool_calls);

☔️ Umbrella reminder if there's rain today

Screenshot 2023-09-14 at 12.31.32.png

Setup

  1. Fork this val 👉 https://val.town/v/stevekrouse.umbrellaReminder/fork
  2. Customize the location (line 8). You can supply any free-form description of a location.

⚠️ Only works for US-based locations (where weather.gov covers).

How it works

  1. Geocodes an free-form description of a location to latitude and longitude – @stevekrouse.nominatimSearch
  2. Converts a latitude and longitude to weather.gov grid – @stevekrouse.weatherGovGrid
  3. Gets the hourly forecast for that grid
  4. Filters the forecast for periods that are today and >30% chance of rain
  5. If there are any, it formats them appropriately, and sends me an email
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { email } from "https://esm.town/v/std/email?v=9";
import { fetchJSON } from "https://esm.town/v/stevekrouse/fetchJSON";
import { nominatimSearch } from "https://esm.town/v/stevekrouse/nominatimSearch";
import { weatherGovGrid } from "https://esm.town/v/stevekrouse/weatherGovGrid";
export const umbrellaReminder = async (arg) => {
if (arg.method) return Response.json("");
let location = "lansing, mi"; // <---- customize this line
let [{ lat, lon }] = await nominatimSearch({
q: location,
});
let { properties: grid } = await weatherGovGrid({
lat,
lon,
});
let { properties: { periods } } = await fetchJSON(
grid.forecastHourly,
);
let { DateTime } = await import("npm:luxon");
let parse = (iso) => DateTime.fromISO(iso).setZone(grid.timeZone);
let today = periods.filter((x) =>
parse(x.startTime).toLocaleString()
=== DateTime.now().setZone(grid.timeZone).toLocaleString()
);
console.log(today);
if (today.every((x) => x.probabilityOfPrecipitation.value < 30))
return today;
let format = (iso) => parse(iso).toFormat("ha").toLowerCase();
let html = `The probabilities of rain in <b>${location}</b> today:<br><br>`
+ today.map((
{ startTime, endTime, probabilityOfPrecipitation: { value: p } },
) => `${format(startTime)}-${format(endTime)}: ${p}%`).join("<br>");
return email({ html, subject: "☔️ Carry an umbrella today!" });
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { latLngOfCity } from "https://esm.town/v/jdan/latLngOfCity";
import { fetchWebpage } from "https://esm.town/v/jdan/fetchWebpage";
import { weatherOfLatLon } from "https://esm.town/v/jdan/weatherOfLatLon";
import { OpenAI } from "https://esm.town/v/std/openai?v=4";
const openai = new OpenAI();
const toolbox = {
"latLngOfCity": {
openAiTool: {
type: "function",
function: {
name: "latLngOfCity",
description: "Get the latitude and longitude of a city",
parameters: {
type: "object",
properties: {
cityName: {
type: "string",
description: "The name of the city",
example: "Hoboken",
},
},
},
response: {
type: "string",
description: "The latitude and longitude of the city",
example: "40°44′42″N 74°01′57″W",
},
},
},
call: latLngOfCity,
},
"weatherOfLatLon": {
openAiTool: {
type: "function",
function: {
name: "weatherOfLatLon",
description: "Get the latitude and longitude of a city",
parameters: {
type: "object",
properties: {
lat: {
type: "number",
description: "The latitude of the city",
},
lon: {
type: "number",
description: "The longitude of the city",
},
},
},
response: {
type: "object",
description: "A large JSON objecft describing the weather",
},
},
},
call: weatherOfLatLon
},
"fetchWebpage": {
openAiTool: {
type: "function",
function: {
name: "fetchWebpage",
description: "Fetch the weather forecast from the contents of a forecast URL",
parameters: {
type: "object",
properties: {
url: {
type: "string"
}
}
},
response: {
type: "string"
}
}
},
call: fetchWebpage
}
};
const tools = Object.values(toolbox).map(({ openAiTool }) => openAiTool);
const transcript = [
{ role: "user", content: "What's the weather in Hoboken, NJ? Do your best to follow URLs and summarize the weather instead of having the user do it." },
];
function truncate(response: string | object) {
const TRUNCATE_LEN = 60
const responseStr = typeof response === "string" ? response : JSON.stringify(response)
return responseStr.length > TRUNCATE_LEN ? responseStr.split("\n").join("").slice(0, TRUNCATE_LEN) + "..." : responseStr
}
async function runConversation() {
const response = await openai.chat.completions.create({
messages: transcript,
tools,
model: "gpt-4-turbo-preview",
});
1
2
3
4
5
6
7
import { weatherGovGrid } from "https://esm.town/v/stevekrouse/weatherGovGrid?v=4";
export async function weatherOfLatLon(args: { lat: number; lon: number }) {
const { lat, lon } = args;
const grid = await weatherGovGrid({ lat, lon });
return grid;
}

If you fork this, you'll need to set OPENAI_API_KEY in your Val Town Secrets.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/** @jsxImportSource https://esm.sh/react */
import { Canvas } from "https://esm.sh/react-three-fiber";
import { useEffect, useState } from "https://esm.sh/react@18.2.0";
import { html } from "https://esm.town/v/stevekrouse/html?v=5";
export function WeatherSuggest() {
// const onClick = () => alert("Button clicked!");
const [data, setData] = useState(null);
useEffect(async () => {
const results = await fetch("https://seflless-weatherGPT.web.val.run/data");
const data = await results.json();
console.log(data);
setData(data);
}, []);
console.log(Canvas);
return !data ? <p>Loading...</p> : <p>{data}</p>;
}
export default async function weatherGPT(req: Request) {
const { OpenAI } = await import("npm:openai");
if (new URL(req.url).pathname === "/data") {
return Response.json({
weather: "Sunny",
});
}
return html(`<html><head></head><body><script type="module">
import { hydrateRoot } from "https://esm.sh/react-dom@18.2.0/client";
import { jsx as _jsx } from "https://esm.sh/react/jsx-runtime";
import { WeatherSuggest } from "https://esm.town/v/seflless/weatherGPT";
let props = {}
hydrateRoot(document, _jsx(WeatherSuggest, props));
</script>
</body>
`);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { OpenAI } from "https://esm.town/v/std/openai?v=2";
const openai = new OpenAI();
const functionExpression = await openai.chat.completions.create({
"messages": [
{ "role": "user", "content": "whats the weather in sf" },
],
tools: [
{
function: {
name: "weather",
parameters: {
"type": "object",
"properties": {
"location": { "type": "string", "description": "The city and state e.g. San Francisco, CA" },
"unit": { "type": "string", "enum": ["c", "f"] },
},
"required": ["location"],
},
},
type: "function",
},
],
model: "gpt-4",
max_tokens: 30,
});
console.log(functionExpression.choices);