Skip to content

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.

pip install idfkit

Weather is an opt-in install and the shared name does not reach it. npm install idfkit places no station index on disk and no weather code in your bundle; add the package by name.

npm install @idfkit/weather

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}")
import { loadBundledIndex } from '@idfkit/weather/node';
import { fetchWeatherFiles } from 'idfkit/weather';

const index = await loadBundledIndex();
const [best] = index.search('chicago ohare');

const files = await fetchWeatherFiles(best.station);
console.log(files.epw.length, files.ddy?.length);

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}")
import { fetchEpwByFilename } from 'idfkit/weather';

const epw = await fetchEpwByFilename('USA_IL_Chicago.Ohare.Intl.AP.725300_TMYx.2009-2023', index);

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:

from pathlib import Path

downloader = WeatherDownloader(cache_dir=Path("/data/weather_cache"))

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.

from idfkit.weather import WeatherDownloader

downloader = WeatherDownloader()

try:
    files = downloader.download(station)
except Exception as e:
    print(f"Download failed: {e}")
try {
  const files = await fetchWeatherFiles(station);
} catch (error) {
  console.error(`Download failed: ${String(error)}`);
}

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