How to download weather files¶
You have a weather station and you need its files: the EPW for an annual run, the DDY for design-day sizing. This guide fetches them, by station or by canonical EPW filename, one at a time or in bulk. To choose the station in the first place, see How to search for weather stations.
One difference runs through everything below. Python downloads into a cache
directory and hands back Path objects. TypeScript writes nothing to disk and
hands back the file contents as text. Neither is a reduced version of the other:
a browser has no filesystem to cache into, and a Python caller is about to pass
a path to EnergyPlus.
In JavaScript, weather is a separate install
pip install idfkit installs weather support and its station index
unconditionally. npm install idfkit installs neither. @idfkit/weather is
an optional peer dependency of the shared idfkit package, so the installer
leaves it out by default and the 1.7 MB station index stays off disk for
everyone who never asks for weather. Importing idfkit/weather without it
fails with a message naming the package to install. See How to install
idfkit,
which also covers the fact that the npm packages are not published yet.
Prefer the shell?
The idfkit tmy CLI wraps the Python API for interactive
use. Pass --download DIR to fetch the EPW/DDY/STAT bundle for a station
without writing any code. There is no JavaScript equivalent.
Differs in JavaScript
Retrieving weather and design-day files exists in both libraries and does not behave the same way in JavaScript. The ledger records it as Python complete, JavaScript partial, and what differs is stated here rather than left to be discovered.
Both libraries retrieve a station's ZIP archive from climate.onebuilding.org and unpack
the EPW, DDY, and STAT members out of it. What they hand back differs, and it differs in
the first line a reader writes. Python's WeatherDownloader.download returns a
WeatherFiles whose epw, ddy, stat, and zip_path are Path objects, because the files are
on disk by the time it returns and a path is what EnergyPlus is given. TypeScript's
fetchWeatherFiles returns a WeatherFiles whose epw, ddy, and stat are the file TEXT,
alongside a members map holding every archive member as bytes, because a browser has no
disk and the text is what @idfkit/engine takes. Same name, different values, which the
naming register records under the retrieved weather files: code written from one
language's documentation is wrong at runtime against the other rather than merely
awkward.
Three things exist on one side only, and each follows from that one fact rather than from a gap. Python's download(station, only={".epw"}) extracts a chosen subset and returns PartialWeatherFiles; selective extraction is a property of writing into a cache, and the JavaScript side decodes from memory whatever the archive held. TypeScript's FetchWeatherOptions carries fetch, rewriteUrl, and signal, because climate.onebuilding.org sends no Access-Control-Allow-Origin header and a page can reach it only through a proxy the caller supplies; Python runs under no same-origin policy and owns its own urllib requests. TypeScript splits retrieving from writing, so @idfkit/weather/node adds saveWeatherFiles and SavedWeatherFiles, where in Python the two are one operation.
Resolving a canonical EPW filename differs in one argument. Python's index parameter is optional and defaults to the bundled index, because it can always find one on disk. TypeScript's is required, because the caller had to obtain an index already and the function has nowhere to load one from.
typescript = "partial" records the missing selective extraction and nothing else. It is
not a verdict on the on-disk cache: that is
weather-file-cache, which is
permanently absent by decision rather than missing.
The full entry, including the vocabulary this capability owns, is on the capability parity page.
How weather is installed¶
Weather comes with the library. pip install idfkit installs the station
index whether or not you asked for it, because a Python extra gates
dependencies rather than files.
Download the files for a station¶
Resolve a station, then ask for its files.
from idfkit.weather import StationIndex, WeatherDownloader
# Find a station
index = StationIndex.load()
station = index.search("chicago ohare")[0].station
# Download weather files
downloader = WeatherDownloader()
files = downloader.download(station)
print(f"EPW: {files.epw}")
print(f"DDY: {files.ddy}")
loadBundledIndex reads the index shipped inside the package, off disk and with
no network call, so it exists only in Node. In a browser, serve the index from
your own origin and load it with loadStationIndex; How to search for weather
stations covers both paths.
Download by canonical EPW filename¶
When you already have the filename, skip the station lookup.
from idfkit.weather import WeatherDownloader
downloader = WeatherDownloader()
# Download directly by EPW filename — no manual station lookup needed
epw = downloader.get_epw_by_filename("USA_IL_Chicago.Ohare.Intl.AP.725300_TMYx.2009-2023")
ddy = downloader.get_ddy_by_filename("USA_IL_Chicago.Ohare.Intl.AP.725300_TMYx.2009-2023")
print(f"EPW: {epw}")
print(f"DDY: {ddy}")
The index does the resolving in both languages, but it arrives differently.
Python's index argument is optional and defaults to the bundled index, because
it can always find one on disk. TypeScript's is required, because the caller had
to obtain an index already and the function has nowhere to load one from. Either
way, a filename matching no station raises rather than returning nothing.
Extract only the files you need¶
By default download() extracts the whole bundle and requires both an EPW and a
DDY to be present. Pass only={".epw"}, or any iterable of suffixes, to extract
just the members you want: useful when the EPW alone will do, or when iterating
over thousands of stations. Matching ignores case, so ".EPW" and "epw" both
select .epw.
# Pull just the EPW out of the bundle — skip DDY and STAT extraction.
files = downloader.download(station, only={".epw"})
print(f"EPW: {files.epw}")
assert files.ddy is None
assert files.stat is None
# Pass an iterable of any-case suffixes; ".EPW" and "epw" both match ".epw".
both = downloader.download(station, only=[".epw", ".ddy"])
assert both.epw is not None
assert both.ddy is not None
There is no TypeScript counterpart, and that is not a gap. Selective extraction is a property of writing into a cache; the JavaScript side decodes from memory whatever the archive held, and carries the undecoded members beside it.
What a download returns¶
The record carries the same station and the same three weather files in both languages, and the fields that matter most hold different values.
| Field | Python (WeatherFiles) |
TypeScript (WeatherFiles) |
|---|---|---|
epw |
Path to the extracted EPW |
the EPW text |
ddy |
Path to the extracted DDY |
the DDY text, or null |
stat |
Path to the STAT file, or None |
the STAT text, or null |
zip_path |
Path to the downloaded ZIP archive |
not present |
members |
not present | every archive member as bytes, by filename |
station |
the WeatherStation downloaded |
the WeatherStation downloaded |
The names collide deliberately: files.epw is a path in Python and the file
contents in TypeScript, so code written from one language's documentation
against the other is wrong at runtime rather than merely awkward. Parity with
the Python library records the difference and its
cause.
files = downloader.download(station)
# Use for simulation
from idfkit.simulation import simulate
result = simulate(model, files.epw)
# Use for design days
from idfkit.weather import DesignDayManager
ddm = DesignDayManager(files.ddy)
Selective extraction returns a PartialWeatherFiles¶
download(station, only=...) returns a PartialWeatherFiles instead: the same
record with epw, ddy, and stat each Path | None. A field is None when
its suffix was neither requested nor already sitting in the cache from an
earlier download, so a suffix you did not ask for this time may still come back
populated.
| Attribute | Type | Description |
|---|---|---|
epw |
Path | None |
Path to the EPW file, or None if not extracted |
ddy |
Path | None |
Path to the DDY file, or None if not extracted |
stat |
Path | None |
Path to the STAT file, or None if not extracted |
zip_path |
Path |
Path to the original downloaded ZIP archive |
station |
WeatherStation |
The station this download corresponds to |
Reuse the cache instead of the network¶
Python caches; TypeScript does not. WeatherDownloader.download() checks the
cache, fetches the station's ZIP only if it has to, extracts the requested
members, stores them, and returns their paths, so the second call for a station
costs no network at all. fetchWeatherFiles re-downloads every time, because
there is nowhere in a browser to put the result. Hold on to the text you were
given.
# First download: fetches from internet
files1 = downloader.download(station)
# Second download: instant from cache
files2 = downloader.download(station)
assert files1.epw == files2.epw # Same cached file
Cache location¶
| Platform | Default path |
|---|---|
| Linux | ~/.cache/idfkit/weather/files/ |
| macOS | ~/Library/Caches/idfkit/weather/files/ |
| Windows | %LOCALAPPDATA%\idfkit\cache\weather\files\ |
Set IDFKIT_CACHE_DIR
to override all three, or pass a directory to one downloader:
Clear the cache¶
# Removes the cached weather files. The station index is kept, so the next
# download resolves stations without going back to the network for the index.
downloader.clear_cache()
Fetch from a page: route around the missing CORS header¶
climate.onebuilding.org sends no Access-Control-Allow-Origin header, so a
direct fetch from a web page is blocked by the browser's same-origin policy.
Python and Node are unaffected; a page needs a forwarding proxy you control that
adds the header. Point rewriteUrl at it:
const epw = await fetchEpw(station, {
rewriteUrl: (url) => `https://your-proxy.example/?url=${encodeURIComponent(url)}`,
});
rewriteUrl only changes the URL that gets fetched, so any forwarding proxy
works. Pass your own fetch instead when you need to add headers or
authentication.
Handle a failed download¶
Both libraries reach the network, so both fail on the things networks fail on: no connectivity, a station URL that no longer resolves, a server that is temporarily down.
Run offline¶
Cached files need no network, so pre-downloading the stations a run will use gets you through the download itself.
# Pre-download files while online
downloader = WeatherDownloader()
for station in my_stations:
downloader.download(station)
# Later, offline usage works
files = downloader.download(station) # From cache
Warming the cache is necessary but not sufficient
StationIndex.load() still fires a throttled freshness check: at most once
every 24 hours it sends a HEAD request for each of the 10 upstream index
files. Offline, every one of them fails slowly and silently, and the first
load of the day can block for minutes before returning an index it already
had. IDFKIT_NO_WEATHER_UPDATE_CHECK=1 is what actually suppresses them,
and the cache has to sit somewhere both the warming environment and the
isolated run can see. How to warm the weather cache for an offline
run covers the whole setup.
There is nothing to warm in JavaScript, because nothing is cached. That
library's offline story is settled at install time instead: install
@idfkit/weather and its bundled index comes with it.
Download for many stations¶
from idfkit.weather import StationIndex, WeatherDownloader
index = StationIndex.load()
downloader = WeatherDownloader()
# Download for multiple cities
cities = ["chicago", "new york", "los angeles", "houston"]
weather_files = {}
for city in cities:
station = index.search(city)[0].station
files = downloader.download(station)
weather_files[city] = files
print(f"Downloaded: {station.display_name}")
What EPW and DDY files contain¶
An EPW holds hourly weather for a typical meteorological year: temperature, humidity, solar radiation, wind, and the rest. It is what an annual simulation reads.
A DDY holds ASHRAE design day conditions as SizingPeriod:DesignDay
objects. It is what HVAC sizing reads. See How to apply design
days for injecting them into a model.
Put the files where the simulation will find them¶
In Python the downloaded paths go straight into simulate():
from idfkit import load_idf
from idfkit.weather import (
StationIndex,
WeatherDownloader,
DesignDayManager,
geocode,
)
from idfkit.simulation import simulate
# Load model
model = load_idf("building.idf")
# Find station near project site
index = StationIndex.load()
lat, lon = geocode("123 Main St, Chicago, IL")
station = index.nearest(lat, lon)[0].station
# Download weather files
downloader = WeatherDownloader()
files = downloader.download(station)
# Apply design days
ddm = DesignDayManager(files.ddy)
ddm.apply_to_model(model, heating="99.6%", cooling="1%")
# Run simulation
result = simulate(model, files.epw, design_day=True)
print(f"Success: {result.success}")
In TypeScript you hold text, so either hand it to an engine that takes text or
write it out first. saveWeatherFiles does the writing, in the Latin-1 encoding
EPW uses:
import { fetchWeatherFiles } from 'idfkit/weather';
import { saveWeatherFiles } from '@idfkit/weather/node';
const files = await fetchWeatherFiles(station);
const saved = await saveWeatherFiles(files, './weather');
console.log(saved.epw);
See also¶
- How to search for weather stations for finding a station, and for loading or refreshing the index
- How to apply design days for using the DDY
- How to warm the weather cache for an offline run for air-gapped and CI environments
- How to install idfkit for the packaging difference in full
- Weather Data Pipeline for where the data comes from and how the index is built