Skip to content

Add iotools functions for Meteonorm #2499

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/sphinx/source/reference/iotools.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ Meteonorm
.. autosummary::
:toctree: generated/

iotools.get_meteonorm
iotools.get_meteonorm_observation
iotools.get_meteonorm_forecast
iotools.get_meteonorm_tmy


Expand Down
3 changes: 2 additions & 1 deletion docs/sphinx/source/whatsnew/v0.13.1.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ Bug fixes
Enhancements
~~~~~~~~~~~~
* Add iotools functions to retrieve irradiance and weather data from Meteonorm:
:py:func:`~pvlib.iotools.get_meteonorm` and :py:func:`~pvlib.iotools.get_meteonorm_tmy`.
:py:func:`~pvlib.iotools.get_meteonorm_observation`, :py:func:`~pvlib.iotools.get_meteonorm_forecast`,
and :py:func:`~pvlib.iotools.get_meteonorm_tmy`.
(:pull:`2499`)
* Add :py:func:`pvlib.iotools.get_nasa_power` to retrieve data from NASA POWER free API.
(:pull:`2500`)
Expand Down
3 changes: 2 additions & 1 deletion pvlib/iotools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
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.meteonorm import get_meteonorm # noqa: F401
from pvlib.iotools.meteonorm import get_meteonorm_observation # noqa: F401
from pvlib.iotools.meteonorm import get_meteonorm_forecast # noqa: F401
from pvlib.iotools.meteonorm import get_meteonorm_tmy # noqa: F401
from pvlib.iotools.nasa_power import get_nasa_power # noqa: F401
154 changes: 133 additions & 21 deletions pvlib/iotools/meteonorm.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,19 @@
}


def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
parameters='all', *, surface_tilt=0, surface_azimuth=180,
time_step='15min', horizon='auto', interval_index=False,
map_variables=True, url=URL):
def get_meteonorm_observation(
latitude, longitude, start, end, api_key, endpoint='training',
parameters='all', *, surface_tilt=0, surface_azimuth=180,
time_step='15min', horizon='auto', interval_index=False,
map_variables=True, url=URL):
"""
Retrieve irradiance and weather data from Meteonorm.
Retrieve historical and near real-time observational data from Meteonorm.

The Meteonorm data options are described in [1]_ and the API is described
in [2]_. A detailed list of API options can be found in [3]_.

This function supports retrieval of historical and forecast data, but not
TMY.
This function supports retrieval of observation data, either the
'training' or the 'realtime' endpoints.

Parameters
----------
Expand All @@ -58,14 +59,11 @@
specified, UTC is assumed.
api_key : str
Meteonorm API key.
endpoint : str
endpoint : str, default : training
API endpoint, see [3]_. Must be one of:

* ``'observation/training'`` - historical data with a 7-day delay
* ``'observation/realtime'`` - near-real time (past 7-days)
* ``'forecast/basic'`` - forecast with hourly resolution
* ``'forecast/precision'`` - forecast with 1-min, 15-min, or hourly
resolution
* ``'training'`` - historical data with a 7-day delay
* ``'realtime'`` - near-real time (past 7-days)

parameters : list or 'all', default : 'all'
List of parameters to request or `'all'` to get all parameters.
Expand All @@ -75,8 +73,7 @@
Orientation (azimuth angle) of the (fixed) plane. Clockwise from north
(north=0, east=90, south=180, west=270).
time_step : {'1min', '15min', '1h'}, default : '15min'
Frequency of the time series. The endpoint ``'forecast/basic'`` only
supports ``time_step='1h'``.
Frequency of the time series.
horizon : str or list, default : 'auto'
Specification of the horizon line. Can be either 'flat', 'auto', or
a list of 360 integer horizon elevation angles.
Expand All @@ -87,8 +84,7 @@
When true, renames columns of the Dataframe to pvlib variable names
where applicable. See variable :const:`VARIABLE_MAP`.
url : str, optional
Base URL of the Meteonorm API. The ``endpoint`` parameter is
appended to the url. The default is
Base URL of the Meteonorm API. The default is
:const:`pvlib.iotools.meteonorm.URL`.

Raises
Expand All @@ -107,11 +103,11 @@
Examples
--------
>>> # Retrieve historical time series data
>>> df, meta = pvlib.iotools.get_meteonorm( # doctest: +SKIP
>>> df, meta = pvlib.iotools.get_meteonorm_observatrion( # doctest: +SKIP
... latitude=50, longitude=10, # doctest: +SKIP
... start='2023-01-01', end='2025-01-01', # doctest: +SKIP
... api_key='redacted', # doctest: +SKIP
... endpoint='observation/training') # doctest: +SKIP
... endpoint='training') # doctest: +SKIP

See Also
--------
Expand All @@ -126,6 +122,120 @@
.. [3] `Meteonorm API reference
<https://docs.meteonorm.com/api>`_
"""
endpoint_base = 'observation/'

data, meta = _get_meteonorm(
latitude, longitude, start, end, api_key,

Check failure on line 128 in pvlib/iotools/meteonorm.py

View workflow job for this annotation

GitHub Actions / flake8-linter

E126 continuation line over-indented for hanging indent
endpoint_base, endpoint,
parameters, surface_tilt, surface_azimuth,
time_step, horizon, interval_index,
map_variables, url)
return data, meta


def get_meteonorm_forecast(
latitude, longitude, start, end, api_key, endpoint='precision',
parameters='all', *, surface_tilt=0, surface_azimuth=180,
time_step='15min', horizon='auto', interval_index=False,
map_variables=True, url=URL):
"""
Retrieve historical and near real-time observational data from Meteonorm.

The Meteonorm data options are described in [1]_ and the API is described
in [2]_. A detailed list of API options can be found in [3]_.

This function supports retrieval of forecasting data, either the
'training' or the 'basic' endpoints.

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. If a timezone is not
specified, UTC is assumed.
end : datetime like
Last timestamp of the requested period. If a timezone is not
specified, UTC is assumed.
api_key : str
Meteonorm API key.
endpoint : str, default : precision
API endpoint, see [3]_. Must be one of:

* ``'precision'`` - forecast with 1-min, 15-min, or hourly
resolution
* ``'basic'`` - forecast with hourly resolution

parameters : list or 'all', default : 'all'
List of parameters to request or `'all'` to get all parameters.
surface_tilt : float, default : 0
Tilt angle from horizontal plane.
surface_azimuth : float, default : 180
Orientation (azimuth angle) of the (fixed) plane. Clockwise from north
(north=0, east=90, south=180, west=270).
time_step : {'1min', '15min', '1h'}, default : '15min'
Frequency of the time series. The endpoint ``'basic'`` only
supports ``time_step='1h'``.
horizon : str or list, default : 'auto'
Specification of the horizon line. Can be either 'flat', 'auto', or
a list of 360 integer horizon elevation angles.
interval_index : bool, default : False
Index is pd.DatetimeIndex when False, and pd.IntervalIndex when True.
This is an experimental feature which may be removed without warning.
map_variables : bool, default : True
When true, renames columns of the Dataframe to pvlib variable names
where applicable. See variable :const:`VARIABLE_MAP`.
url : str, optional
Base URL of the Meteonorm API. The default is
:const:`pvlib.iotools.meteonorm.URL`.

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 unless ``interval_index`` is set to True.
meta : dict
Metadata.

See Also
--------
pvlib.iotools.get_meteonorm_observation,
pvlib.iotools.get_meteonorm_tmy

References
----------
.. [1] `Meteonorm
<https://meteonorm.com/>`_
.. [2] `Meteonorm API
<https://docs.meteonorm.com/docs/getting-started>`_
.. [3] `Meteonorm API reference
<https://docs.meteonorm.com/api>`_
"""
endpoint_base = 'forecast/'

data, meta = _get_meteonorm(
latitude, longitude, start, end, api_key,

Check failure on line 224 in pvlib/iotools/meteonorm.py

View workflow job for this annotation

GitHub Actions / flake8-linter

E126 continuation line over-indented for hanging indent
endpoint_base, endpoint,
parameters, surface_tilt, surface_azimuth,
time_step, horizon, interval_index,
map_variables, url)
return data, meta


def _get_meteonorm(
latitude, longitude, start, end, api_key,
endpoint_base, endpoint,
parameters, surface_tilt, surface_azimuth,
time_step, horizon, interval_index,
map_variables, url):

# Relative date strings are not yet supported
start = pd.Timestamp(start)
end = pd.Timestamp(end)
Expand Down Expand Up @@ -167,7 +277,8 @@
headers = {"Authorization": f"Bearer {api_key}"}

response = requests.get(
urljoin(url, endpoint.lstrip('/')), headers=headers, params=params)
urljoin(url, endpoint_base + endpoint.lstrip('/')),
headers=headers, params=params)

if not response.ok:
# response.raise_for_status() does not give a useful error message
Expand Down Expand Up @@ -265,7 +376,8 @@

See Also
--------
pvlib.iotools.get_meteonorm
pvlib.iotools.get_meteonorm_observation,
pvlib.iotools.get_meteonorm_forecast

References
----------
Expand Down
30 changes: 15 additions & 15 deletions tests/iotools/test_meteonorm.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,12 @@ def expected_columns_all():
def test_get_meteonorm_training(
demo_api_key, demo_url, expected_meta, expected_meteonorm_index,
expected_metenorm_data):
data, meta = pvlib.iotools.get_meteonorm(
data, meta = pvlib.iotools.get_meteonorm_observation(
latitude=50, longitude=10,
start='2023-01-01', end='2025-01-01',
api_key=demo_api_key,
parameters=['ghi', 'global_horizontal_irradiance_with_shading'],
endpoint='observation/training',
endpoint='training',
time_step='1h',
url=demo_url)

Expand All @@ -128,14 +128,14 @@ def test_get_meteonorm_training(
@pytest.mark.remote_data
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
def test_get_meteonorm_realtime(demo_api_key, demo_url, expected_columns_all):
data, meta = pvlib.iotools.get_meteonorm(
data, meta = pvlib.iotools.get_meteonorm_observation(
latitude=21, longitude=79,
start=pd.Timestamp.now(tz='UTC') - pd.Timedelta(hours=5),
end=pd.Timestamp.now(tz='UTC') - pd.Timedelta(hours=1),
surface_tilt=20, surface_azimuth=10,
parameters=['all'],
api_key=demo_api_key,
endpoint='/observation/realtime',
endpoint='realtime',
time_step='1min',
horizon='flat',
map_variables=False,
Expand All @@ -158,14 +158,14 @@ def test_get_meteonorm_realtime(demo_api_key, demo_url, expected_columns_all):
@pytest.mark.remote_data
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
def test_get_meteonorm_forecast_basic(demo_api_key, demo_url):
data, meta = pvlib.iotools.get_meteonorm(
data, meta = pvlib.iotools.get_meteonorm_forecast(
latitude=50, longitude=10,
start=pd.Timestamp.now(tz='UTC'),
end=pd.Timestamp.now(tz='UTC') + pd.Timedelta(hours=5),
time_step='1h',
api_key=demo_api_key,
parameters='ghi',
endpoint='forecast/basic',
endpoint='basic',
url=demo_url)

assert data.shape == (6, 1)
Expand All @@ -177,13 +177,13 @@ def test_get_meteonorm_forecast_basic(demo_api_key, demo_url):
@pytest.mark.remote_data
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
def test_get_meteonorm_forecast_precision(demo_api_key, demo_url):
data, meta = pvlib.iotools.get_meteonorm(
data, meta = pvlib.iotools.get_meteonorm_forecast(
latitude=50, longitude=10,
start=pd.Timestamp.now(tz='UTC') + pd.Timedelta(hours=5),
end=pd.Timestamp.now(tz='UTC') + pd.Timedelta(hours=6),
api_key=demo_api_key,
parameters='ghi',
endpoint='forecast/precision',
endpoint='precision',
time_step='15min',
url=demo_url)

Expand All @@ -195,45 +195,45 @@ def test_get_meteonorm_forecast_precision(demo_api_key, demo_url):
@pytest.mark.remote_data
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
def test_get_meteonorm_custom_horizon(demo_api_key, demo_url):
data, meta = pvlib.iotools.get_meteonorm(
data, meta = pvlib.iotools.get_meteonorm_forecast(
latitude=50, longitude=10,
start=pd.Timestamp.now(tz='UTC'),
end=pd.Timestamp.now(tz='UTC') + pd.Timedelta(hours=5),
api_key=demo_api_key,
parameters='ghi',
time_step='1h',
endpoint='forecast/basic',
endpoint='basic',
horizon=list(np.ones(360).astype(int)*80),
url=demo_url)


@pytest.mark.remote_data
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
def test_get_meteonorm_HTTPError(demo_api_key, demo_url):
def test_get_meteonorm_forecast_HTTPError(demo_api_key, demo_url):
with pytest.raises(
HTTPError, match="unknown parameter: not_a_real_parameter"):
_ = pvlib.iotools.get_meteonorm(
_ = pvlib.iotools.get_meteonorm_forecast(
latitude=50, longitude=10,
start=pd.Timestamp.now(tz='UTC'),
end=pd.Timestamp.now(tz='UTC') + pd.Timedelta(hours=5),
time_step='1h',
api_key=demo_api_key,
parameters='not_a_real_parameter',
endpoint='forecast/basic',
endpoint='basic',
url=demo_url)


def test_get_meteonorm_basic_forecast_incorrect_time_step(
demo_api_key, demo_url):
with pytest.raises(
ValueError, match="only supports ``time_step='1h'``"):
_ = pvlib.iotools.get_meteonorm(
_ = pvlib.iotools.get_meteonorm_forecast(
latitude=50, longitude=10,
start=pd.Timestamp.now(tz='UTC'),
end=pd.Timestamp.now(tz='UTC') + pd.Timedelta(hours=5),
time_step='15min', # only '1h' is supported for tmy
api_key=demo_api_key,
endpoint='forecast/basic',
endpoint='basic',
url=demo_url)


Expand Down
Loading