diff --git a/docs/sphinx/source/reference/iotools.rst b/docs/sphinx/source/reference/iotools.rst
index cbf89c71a7..bcc9f4e2a6 100644
--- a/docs/sphinx/source/reference/iotools.rst
+++ b/docs/sphinx/source/reference/iotools.rst
@@ -57,6 +57,17 @@ clear-sky irradiance globally.
iotools.parse_cams
+NASA POWER
+**********
+
+Satellite-derived irradiance and weather data with global coverage.
+
+.. autosummary::
+ :toctree: generated/
+
+ iotools.get_nasa_power
+
+
NSRDB
*****
diff --git a/docs/sphinx/source/whatsnew/v0.13.1.rst b/docs/sphinx/source/whatsnew/v0.13.1.rst
index 9c50d00bbb..ad798edc0f 100644
--- a/docs/sphinx/source/whatsnew/v0.13.1.rst
+++ b/docs/sphinx/source/whatsnew/v0.13.1.rst
@@ -19,7 +19,8 @@ Bug fixes
Enhancements
~~~~~~~~~~~~
-
+* Added function :py:func:`~pvlib.iotools.get_nasa_power` to retrieve data from NASA POWER.
+ (:pull:`2500`)
Documentation
~~~~~~~~~~~~~
@@ -45,3 +46,5 @@ Maintenance
Contributors
~~~~~~~~~~~~
* Elijah Passmore (:ghuser:`eljpsm`)
+* Ioannis Sifnaios (:ghuser:`IoannisSifnaios`)
+
diff --git a/pvlib/iotools/__init__.py b/pvlib/iotools/__init__.py
index 352044e5cd..51136ce9bc 100644
--- a/pvlib/iotools/__init__.py
+++ b/pvlib/iotools/__init__.py
@@ -39,3 +39,4 @@
from pvlib.iotools.solcast import get_solcast_historic # noqa: F401
from pvlib.iotools.solcast import get_solcast_tmy # noqa: F401
from pvlib.iotools.solargis import get_solargis # noqa: F401
+from pvlib.iotools.nasa_power import get_nasa_power # noqa: F401
diff --git a/pvlib/iotools/nasa_power.py b/pvlib/iotools/nasa_power.py
new file mode 100644
index 0000000000..6a9b01f10c
--- /dev/null
+++ b/pvlib/iotools/nasa_power.py
@@ -0,0 +1,153 @@
+"""Functions for reading and retrieving data from NASA POWER."""
+
+import pandas as pd
+import requests
+import numpy as np
+
+URL = 'https://power.larc.nasa.gov/api/temporal/hourly/point'
+
+DEFAULT_PARAMETERS = [
+ 'ALLSKY_SFC_SW_DNI', 'ALLSKY_SFC_SW_DIFF', 'ALLSKY_SFC_SW_DWN',
+ 'T2M', 'WS10M'
+]
+
+VARIABLE_MAP = {
+ 'ALLSKY_SFC_SW_DWN': 'ghi',
+ 'ALLSKY_SFC_SW_DIFF': 'dhi',
+ 'ALLSKY_SFC_SW_DNI': 'dni',
+ 'CLRSKY_SFC_SW_DWN': 'ghi_clear',
+ 'T2M': 'temp_air',
+ 'WS2M': 'wind_speed_2m',
+ 'WS10M': 'wind_speed',
+}
+
+
+def get_nasa_power(latitude, longitude, start, end,
+ parameters=DEFAULT_PARAMETERS, community='re', url=URL,
+ elevation=None, wind_height=None, wind_surface=None,
+ map_variables=True):
+ """
+ Retrieve irradiance and weather data from NASA POWER.
+
+ A general description of NASA POWER is given in [1]_ and the API is
+ described in [2]_. A detailed list of the available parameters can be
+ found in [3]_.
+
+ Parameters
+ ----------
+ latitude: float
+ In decimal degrees, north is positive (ISO 19115).
+ longitude: float
+ In decimal degrees, east is positive (ISO 19115).
+ start: datetime like
+ First timestamp of the requested period.
+ end: datetime like
+ Last timestamp of the requested period.
+ parameters: str, list
+ List of parameters. The default parameters are mentioned below; for the
+ full list see [3]_. Note that the pvlib naming conventions can also be
+ used.
+
+ * ``ALLSKY_SFC_SW_DWN``: Global Horizontal Irradiance (GHI) [Wm⁻²]
+ * ``ALLSKY_SFC_SW_DIFF``: Diffuse Horizontal Irradiance (DHI) [Wm⁻²]
+ * ``ALLSKY_SFC_SW_DNI``: Direct Normal Irradiance (DNI) [Wm⁻²]
+ * ``T2M``: Air temperature at 2 m [C]
+ * ``WS10M``: Wind speed at 10 m [m/s]
+
+ community: str
+ Can be one of the following depending on which parameters are of
+ interest. The default is ``'re'``. Note that in many cases this choice
+ might affect the units of the parameter.
+
+ * ``'re'``: renewable energy
+ * ``'sb'``: sustainable buildings
+ * ``'ag'``: agroclimatology
+
+ elevation: float, optional
+ The custom site elevation in meters to produce the corrected
+ atmospheric pressure adjusted for elevation.
+ wind_height: float, optional
+ The custom wind height in meters to produce the wind speed adjusted
+ for height. Has to be between 10 and 300 m; see [4]_.
+ wind_surface: str, optional
+ The definable surface type to adjust the wind speed. For a list of the
+ surface types see [4]_. If you provide a wind surface alias please
+ include a site elevation with the request.
+ map_variables: bool, optional
+ When true, renames columns of the Dataframe to pvlib variable names
+ where applicable. See variable :const:`VARIABLE_MAP`.
+ The default is `True`.
+
+ Raises
+ ------
+ requests.HTTPError
+ Raises an error when an incorrect request is made.
+
+ Returns
+ -------
+ data : pd.DataFrame
+ Time series data. The index corresponds to the start (left) of the
+ interval.
+ meta : dict
+ Metadata.
+
+ References
+ ----------
+ .. [1] `NASA Prediction Of Worldwide Energy Resources (POWER)
+ `_
+ .. [2] `NASA POWER API
+ `_
+ .. [3] `NASA POWER API parameters
+ `_
+ .. [4] `NASA POWER corrected wind speed parameters
+ `_
+ """
+ start = pd.Timestamp(start)
+ end = pd.Timestamp(end)
+
+ # allow the use of pvlib parameter names
+ parameter_dict = {v: k for k, v in VARIABLE_MAP.items()}
+ parameters = [parameter_dict.get(p, p) for p in parameters]
+
+ params = {
+ 'latitude': latitude,
+ 'longitude': longitude,
+ 'start': start.strftime('%Y%m%d'),
+ 'end': end.strftime('%Y%m%d'),
+ 'community': community,
+ 'parameters': ','.join(parameters), # make parameters in a string
+ 'format': 'json',
+ 'user': None,
+ 'header': True,
+ 'time-standard': 'utc',
+ 'site-elevation': elevation,
+ 'wind-elevation': wind_height,
+ 'wind-surface': wind_surface,
+ }
+
+ response = requests.get(url, params=params)
+ if not response.ok:
+ # response.raise_for_status() does not give a useful error message
+ raise requests.HTTPError(response.json())
+
+ # Parse the data to dataframe
+ data = response.json()
+ hourly_data = data['properties']['parameter']
+ df = pd.DataFrame(hourly_data)
+ df.index = pd.to_datetime(df.index, format='%Y%m%d%H').tz_localize('UTC')
+ df = df.replace(-999, np.nan)
+
+ # Create metadata dictionary
+ meta = data['header']
+ meta['times'] = data['times']
+ meta['parameters'] = data['parameters']
+
+ meta['longitude'] = data['geometry']['coordinates'][0]
+ meta['latitude'] = data['geometry']['coordinates'][1]
+ meta['altitude'] = data['geometry']['coordinates'][2]
+
+ # Rename according to pvlib convention
+ if map_variables:
+ df = df.rename(columns=VARIABLE_MAP)
+
+ return df, meta
diff --git a/tests/iotools/test_nasa_power.py b/tests/iotools/test_nasa_power.py
new file mode 100644
index 0000000000..ef175c7e37
--- /dev/null
+++ b/tests/iotools/test_nasa_power.py
@@ -0,0 +1,88 @@
+import pandas as pd
+import pytest
+import pvlib
+from requests.exceptions import HTTPError
+
+
+@pytest.fixture
+def data_index():
+ index = pd.date_range(start='2025-02-02 00:00+00:00',
+ end='2025-02-02 23:00+00:00', freq='1h')
+ return index
+
+
+@pytest.fixture
+def ghi_series(data_index):
+ ghi = [
+ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 50.25, 184.2, 281.55, 368.3, 406.48,
+ 386.45, 316.05, 210.1, 109.05, 12.9, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
+ ]
+ return pd.Series(data=ghi, index=data_index, name='ghi')
+
+
+def test_get_nasa_power(data_index, ghi_series):
+ data, meta = pvlib.iotools.get_nasa_power(latitude=44.76,
+ longitude=7.64,
+ start=data_index[0],
+ end=data_index[-1],
+ map_variables=False)
+ # Check that metadata is correct
+ assert meta['latitude'] == 44.76
+ assert meta['longitude'] == 7.64
+ assert meta['altitude'] == 705.88
+ assert meta['start'] == '20250202'
+ assert meta['end'] == '20250202'
+ assert meta['time_standard'] == 'UTC'
+ assert meta['title'] == 'NASA/POWER Source Native Resolution Hourly Data'
+ # Assert that the index is parsed correctly
+ pd.testing.assert_index_equal(data.index, data_index)
+ # Test one column
+ pd.testing.assert_series_equal(data['ALLSKY_SFC_SW_DWN'], ghi_series,
+ check_freq=False, check_names=False)
+
+
+def test_get_nasa_power_pvlib_params_naming(data_index, ghi_series):
+ data, meta = pvlib.iotools.get_nasa_power(latitude=44.76,
+ longitude=7.64,
+ start=data_index[0],
+ end=data_index[-1],
+ parameters=['ghi'])
+ # Assert that the index is parsed correctly
+ pd.testing.assert_index_equal(data.index, data_index)
+ # Test one column
+ pd.testing.assert_series_equal(data['ghi'], ghi_series,
+ check_freq=False)
+
+
+def test_get_nasa_power_map_variables(data_index):
+ # Check that variables are mapped by default to pvlib names
+ data, meta = pvlib.iotools.get_nasa_power(latitude=44.76,
+ longitude=7.64,
+ start=data_index[0],
+ end=data_index[-1])
+ mapped_column_names = ['ghi', 'dni', 'dhi', 'temp_air', 'wind_speed']
+ for c in mapped_column_names:
+ assert c in data.columns
+ assert meta['latitude'] == 44.76
+ assert meta['longitude'] == 7.64
+ assert meta['altitude'] == 705.88
+
+
+def test_get_nasa_power_wrong_parameter_name(data_index):
+ # Test if HTTPError is raised if a wrong parameter name is asked
+ with pytest.raises(HTTPError, match=r"ALLSKY_SFC_SW_DLN"):
+ pvlib.iotools.get_nasa_power(latitude=44.76,
+ longitude=7.64,
+ start=data_index[0],
+ end=data_index[-1],
+ parameters=['ALLSKY_SFC_SW_DLN'])
+
+
+def test_get_nasa_power_duplicate_parameter_name(data_index):
+ # Test if HTTPError is raised if a duplicate parameter is asked
+ with pytest.raises(HTTPError, match=r"ALLSKY_SFC_SW_DWN"):
+ pvlib.iotools.get_nasa_power(latitude=44.76,
+ longitude=7.64,
+ start=data_index[0],
+ end=data_index[-1],
+ parameters=2*['ALLSKY_SFC_SW_DWN'])