From 7ca85bb063f760eaee443a2d601802f144e6b2de Mon Sep 17 00:00:00 2001
From: "Adam R. Jensen" <39184289+AdamRJensen@users.noreply.github.com>
Date: Tue, 15 Jul 2025 09:44:36 +0200
Subject: [PATCH 01/18] Create meteonorm.py
---
pvlib/iotools/meteonorm.py | 179 +++++++++++++++++++++++++++++++++++++
1 file changed, 179 insertions(+)
create mode 100644 pvlib/iotools/meteonorm.py
diff --git a/pvlib/iotools/meteonorm.py b/pvlib/iotools/meteonorm.py
new file mode 100644
index 0000000000..d9897b8ceb
--- /dev/null
+++ b/pvlib/iotools/meteonorm.py
@@ -0,0 +1,179 @@
+"""Functions for reading and retrieving data from Meteonorm."""
+
+import pandas as pd
+import requests
+from urllib.parse import urljoin
+
+URL = 'https://api.meteonorm.com/v1/'
+
+VARIABLE_MAP = {
+ 'global_horizontal_irradiance': 'ghi',
+ 'diffuse_horizontal_irradiance': 'dhi',
+ 'direct_normal_irradiance': 'dni',
+ 'direct_horizontal_irradiance': 'bhi',
+ 'global_clear_sky_irradiance': 'ghi_clear',
+ 'diffuse_tilted_irradiance': 'poa_diffuse',
+ 'direct_tilted_irradiance': 'poa_direct',
+ 'global_tilted_irradiance': 'poa',
+ 'temperature': 'temp_air',
+ 'dew_point_temperature': 'temp_dew',
+}
+
+time_step_map = {
+ '1h': '1_hour',
+ 'h': '1_hour',
+ '15min': '15_minutes',
+ '1min': '1_minute',
+ 'min': '1_minute',
+}
+
+
+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):
+ """
+ Retrieve irradiance and weather 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 the end points 'realtime' for data for the past 7
+ days, 'training' for historical data with a delay of 7 days. The function
+ does not support TMY climate data.
+
+ Parameters
+ ----------
+ latitude: float
+ In decimal degrees, north is positive (ISO 19115).
+ longitude: float
+ In decimal degrees, east is positive (ISO 19115).
+ start: datetime like, optional
+ First timestamp of the requested period. If a timezone is not
+ specified, UTC is assumed. A relative datetime string is also allowed.
+ end: datetime like, optional
+ Last timestamp of the requested period. If a timezone is not
+ specified, UTC is assumed. A relative datetime string is also allowed.
+ api_key: str
+ Meteonorm API key.
+ endpoint : str
+ API end point, see [3]_. Must be one of:
+
+ * '/observation/training'
+ * '/observation/realtime'
+ * '/forecast/basic'
+ * '/forecast/precision'
+
+ parameters : list, optional
+ List of parameters to request or "all" to get all parameters. The
+ default is "all".
+ 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'}, optional
+ ime step of the time series. The default is '15min'. Ignored if
+ requesting forecast data.
+ horizon : optional
+ Specification of the hoirzon line. Can be either 'flat' or 'auto', or
+ specified as a list of 360 horizon elevation angles. The default is
+ 'auto'.
+ interval_index: bool, optional
+ Whether the index of the returned data object is of the type
+ pd.DatetimeIndex or pd.IntervalIndex. This is an experimental feature
+ which may be removed without warning. The default is False.
+ map_variables: bool, default: True
+ When true, renames columns of the Dataframe to pvlib variable names
+ where applicable. The default is True. See variable
+ :const:`VARIABLE_MAP`.
+ url: str, default: :const:`pvlib.iotools.meteonorm.URL`
+ Base url of the Meteonorm API. The ``endpoint`` parameter is
+ appended to the 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.
+ meta : dict
+ Metadata.
+
+ See Also
+ --------
+ pvlib.iotools.get_meteonorm_tmy
+
+ References
+ ----------
+ .. [1] `Meteonorm
+ `_
+ .. [2] `Meteonorm API
+ `_
+ .. [3] `Meteonorm API reference
+ `_
+ """
+ start = pd.Timestamp(start)
+ end = pd.Timestamp(end)
+ start = start.tz_localize('UTC') if start.tzinfo is None else start
+ end = end.tz_localize('UTC') if end.tzinfo is None else end
+
+ params = {
+ 'lat': latitude,
+ 'lon': longitude,
+ 'start': start.strftime('%Y-%m-%dT%H:%M:%SZ'),
+ 'end': end.strftime('%Y-%m-%dT%H:%M:%SZ'),
+ 'surface_tilt': surface_tilt,
+ 'surface_azimuth': surface_azimuth,
+ 'horizon': horizon,
+ 'parameters': parameters,
+ }
+
+ if 'forecast' not in endpoint.lower():
+ params['frequency'] = time_step_map.get(time_step, time_step)
+
+ # convert list to string with values separated by commas
+ if not isinstance(params['parameters'], (str, type(None))):
+ # 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['parameters'] = ','.join(parameters)
+
+ headers = {"Authorization": f"Bearer {api_key}"}
+
+ response = requests.get(urljoin(url, endpoint), headers=headers, params=params)
+
+ if not response.ok:
+ # response.raise_for_status() does not give a useful error message
+ raise requests.HTTPError(response.json())
+
+ data_json = response.json()['values']
+ # identify empty columns
+ empty_columns = [k for k, v in data_json.items() if v is None]
+ # remove empty columns
+ _ = [data_json.pop(k) for k in empty_columns]
+
+ data = pd.DataFrame(data_json)
+
+ # xxx: experimental feature - see parameter description
+ if interval_index:
+ data.index = pd.IntervalIndex.from_arrays(
+ left=pd.to_datetime(response.json()['start_times']),
+ right=pd.to_datetime(response.json()['end_times']),
+ closed='both',
+ )
+ else:
+ data.index = pd.to_datetime(response.json()['start_times'])
+
+ meta = response.json()['meta']
+
+ if map_variables:
+ data = data.rename(columns=VARIABLE_MAP)
+ meta['latitude'] = meta.pop('lat')
+ meta['longitude'] = meta.pop('lon')
+
+ return data, meta
From 95c260245976c42dd9a62c372529d92f20f445bb Mon Sep 17 00:00:00 2001
From: "Adam R. Jensen" <39184289+AdamRJensen@users.noreply.github.com>
Date: Tue, 15 Jul 2025 13:07:02 +0200
Subject: [PATCH 02/18] Add get_meteonorm_tmy
---
docs/sphinx/source/reference/iotools.rst | 10 ++
docs/sphinx/source/whatsnew/v0.13.1.rst | 4 +-
pvlib/iotools/__init__.py | 2 +
pvlib/iotools/meteonorm.py | 208 +++++++++++++++++++++--
4 files changed, 205 insertions(+), 19 deletions(-)
diff --git a/docs/sphinx/source/reference/iotools.rst b/docs/sphinx/source/reference/iotools.rst
index cbf89c71a7..39ec0f70e9 100644
--- a/docs/sphinx/source/reference/iotools.rst
+++ b/docs/sphinx/source/reference/iotools.rst
@@ -81,6 +81,16 @@ Commercial datasets
Accessing these APIs typically requires payment.
Datasets provide near-global coverage.
+Meteonorm
+*********
+
+.. autosummary::
+ :toctree: generated/
+
+ iotools.get_meteonorm
+ iotools.get_meteonorm_tmy
+
+
SolarAnywhere
*************
diff --git a/docs/sphinx/source/whatsnew/v0.13.1.rst b/docs/sphinx/source/whatsnew/v0.13.1.rst
index 9c50d00bbb..fd6b89cbcb 100644
--- a/docs/sphinx/source/whatsnew/v0.13.1.rst
+++ b/docs/sphinx/source/whatsnew/v0.13.1.rst
@@ -19,7 +19,9 @@ 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`.
+ (:pull:`2499`)
Documentation
~~~~~~~~~~~~~
diff --git a/pvlib/iotools/__init__.py b/pvlib/iotools/__init__.py
index 352044e5cd..e3ecd441cd 100644
--- a/pvlib/iotools/__init__.py
+++ b/pvlib/iotools/__init__.py
@@ -39,3 +39,5 @@
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_tmy # noqa: F401
diff --git a/pvlib/iotools/meteonorm.py b/pvlib/iotools/meteonorm.py
index d9897b8ceb..08a94ea2a5 100644
--- a/pvlib/iotools/meteonorm.py
+++ b/pvlib/iotools/meteonorm.py
@@ -29,7 +29,7 @@
def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
- parameters="all", *, surface_tilt=0, surface_azimuth=180,
+ parameters='all', *, surface_tilt=0, surface_azimuth=180,
time_step='15min', horizon='auto', interval_index=False,
map_variables=True, url=URL):
"""
@@ -38,9 +38,7 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
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 the end points 'realtime' for data for the past 7
- days, 'training' for historical data with a delay of 7 days. The function
- does not support TMY climate data.
+ This function supports both historical and forecast data, but not TMY.
Parameters
----------
@@ -57,15 +55,15 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
api_key: str
Meteonorm API key.
endpoint : str
- API end point, see [3]_. Must be one of:
+ API endpoint, see [3]_. Must be one of:
- * '/observation/training'
- * '/observation/realtime'
- * '/forecast/basic'
- * '/forecast/precision'
+ * '/observation/training' - historical data with a 7-day delay
+ * '/observation/realtime' - near-real time (past 7-days)
+ * '/forecast/basic' - forcasts with hourly resolution
+ * '/forecast/precision' - forecsat with 15-min resolution
parameters : list, optional
- List of parameters to request or "all" to get all parameters. The
+ List of parameters to request or 'all' to get all parameters. The
default is "all".
surface_tilt: float, default: 0
Tilt angle from horizontal plane.
@@ -73,12 +71,11 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
Orientation (azimuth angle) of the (fixed) plane. Clockwise from north
(north=0, east=90, south=180, west=270).
time_step : {'1min', '15min', '1h'}, optional
- ime step of the time series. The default is '15min'. Ignored if
- requesting forecast data.
+ Frequency of the time series. The default is '15min'. The parameter is
+ ignored if forcasting data is requested.
horizon : optional
- Specification of the hoirzon line. Can be either 'flat' or 'auto', or
- specified as a list of 360 horizon elevation angles. The default is
- 'auto'.
+ Specification of the horizon line. Can be either a flat, 'auto', or
+ a list of 360 horizon elevation angles. The default is 'auto'.
interval_index: bool, optional
Whether the index of the returned data object is of the type
pd.DatetimeIndex or pd.IntervalIndex. This is an experimental feature
@@ -87,9 +84,10 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
When true, renames columns of the Dataframe to pvlib variable names
where applicable. The default is True. See variable
:const:`VARIABLE_MAP`.
- url: str, default: :const:`pvlib.iotools.meteonorm.URL`
+ url: str, optional
Base url of the Meteonorm API. The ``endpoint`` parameter is
- appended to the url.
+ appended to the url. The default is
+ :const:`pvlib.iotools.meteonorm.URL`.
Raises
------
@@ -145,7 +143,181 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
headers = {"Authorization": f"Bearer {api_key}"}
- response = requests.get(urljoin(url, endpoint), headers=headers, params=params)
+ response = requests.get(
+ urljoin(url, endpoint), headers=headers, params=params)
+
+ if not response.ok:
+ # response.raise_for_status() does not give a useful error message
+ raise requests.HTTPError(response.json())
+
+ data_json = response.json()['values']
+ # identify empty columns
+ empty_columns = [k for k, v in data_json.items() if v is None]
+ # remove empty columns
+ _ = [data_json.pop(k) for k in empty_columns]
+
+ data = pd.DataFrame(data_json)
+
+ # xxx: experimental feature - see parameter description
+ if interval_index:
+ data.index = pd.IntervalIndex.from_arrays(
+ left=pd.to_datetime(response.json()['start_times']),
+ right=pd.to_datetime(response.json()['end_times']),
+ closed='both',
+ )
+ else:
+ data.index = pd.to_datetime(response.json()['start_times'])
+
+ meta = response.json()['meta']
+
+ if map_variables:
+ data = data.rename(columns=VARIABLE_MAP)
+ meta['latitude'] = meta.pop('lat')
+ meta['longitude'] = meta.pop('lon')
+
+ return data, meta
+
+
+def get_meteonorm_tmy(latitude, longitude, api_key,
+ parameters='all', *, surface_tilt=0,
+ surface_azimuth=180, time_step='15min', horizon='auto',
+ terrain='open', albedo=0.2, turbidity='auto',
+ random_seed=None, clear_sky_radiation_model='esra',
+ data_version='latest', future_scenario=None,
+ future_year=None, interval_index=False,
+ map_variables=True, url=URL):
+ """
+ Retrieve irradiance and weather 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 the endpoints 'realtime' for data for the past 7
+ days, 'training' for historical data with a delay of 7 days. The function
+ does not support TMY climate data.
+
+ Parameters
+ ----------
+ latitude: float
+ In decimal degrees, north is positive (ISO 19115).
+ longitude: float
+ In decimal degrees, east is positive (ISO 19115).
+ api_key: str
+ Meteonorm API key.
+ parameters: list, optional
+ List of parameters to request or 'all' to get all parameters. The
+ default is 'all'.
+ 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', '1h'}, optional
+ Frequency of the time series. The default is '1h'.
+ horizon: optional
+ Specification of the hoirzon line. Can be either 'flat' or 'auto', or
+ specified as a list of 360 horizon elevation angles. The default is
+ 'auto'.
+ terrain: string, optional
+ Local terrain situation. Must be one of: ['open', 'depression',
+ 'cold_air_lake', 'sea_lake', 'city', 'slope_south',
+ 'slope_west_east']. The default is 'open'.
+ albedo: float, optional
+ Ground albedo. Albedo changes due to snow fall are modelled. The
+ default is 0.2.
+ turbidity: list or 'auto', optional
+ List of 12 monthly mean atmospheric Linke turbidity values. The default
+ is 'auto'.
+ random_seed: int, optional
+ Random seed to be used for stochastic processes. Two identical requests
+ with the same random seed will yield identical results.
+ clear_sky_radiation_model : {'esra', 'solis'}
+ Which clearsky model to use. The default is 'esra'.
+ data_version : string, optional
+ Version of Meteonorm climatological data to be used. The default is
+ 'latest'.
+ future_scenario: string, optional
+ Future climate scenario.
+ future_year : integer, optional
+ Central year for a 20-year reference period in the future.
+ interval_index: bool, optional
+ Whether the index of the returned data object is of the type
+ pd.DatetimeIndex or pd.IntervalIndex. This is an experimental feature
+ which may be removed without warning. The default is False.
+ map_variables: bool, default: True
+ When true, renames columns of the Dataframe to pvlib variable names
+ where applicable. The default is True. See variable
+ :const:`VARIABLE_MAP`.
+ url: str, optional.
+ Base url of the Meteonorm API. 'climate/tmy'` is
+ appended to the url. 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.
+ meta : dict
+ Metadata.
+
+ See Also
+ --------
+ pvlib.iotools.get_meteonorm
+
+ References
+ ----------
+ .. [1] `Meteonorm
+ `_
+ .. [2] `Meteonorm API
+ `_
+ .. [3] `Meteonorm API reference
+ `_
+ """
+ params = {
+ 'lat': latitude,
+ 'lon': longitude,
+ 'surface_tilt': surface_tilt,
+ 'surface_azimuth': surface_azimuth,
+ 'frequency': time_step,
+ 'parameters': parameters,
+ 'horizon': horizon,
+ 'terrain': terrain,
+ 'turbidity': turbidity,
+ 'clear_sky_radiation_model': clear_sky_radiation_model,
+ 'data_version': data_version,
+ }
+
+ if turbidity != 'auto':
+ params['turbidity'] = ','.join(turbidity)
+
+ if random_seed is not None:
+ params['random_seed'] = random_seed
+
+ if future_scenario is not None:
+ params['future_scenario'] = future_scenario
+
+ if future_year is not None:
+ params['future_year'] = future_year
+
+ # convert list to string with values separated by commas
+ if not isinstance(params['parameters'], (str, type(None))):
+ # 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['parameters'] = ','.join(parameters)
+
+ headers = {"Authorization": f"Bearer {api_key}"}
+
+ endpoint = 'climate/tmy'
+
+ response = requests.get(
+ urljoin(url, endpoint), headers=headers, params=params)
if not response.ok:
# response.raise_for_status() does not give a useful error message
From 611294ec7909345c2314a4f80d4c2c23e08721e5 Mon Sep 17 00:00:00 2001
From: "Adam R. Jensen" <39184289+AdamRJensen@users.noreply.github.com>
Date: Wed, 16 Jul 2025 13:41:21 +0200
Subject: [PATCH 03/18] Add private shared parse function
---
pvlib/iotools/meteonorm.py | 126 +++++++++++++++++--------------------
1 file changed, 56 insertions(+), 70 deletions(-)
diff --git a/pvlib/iotools/meteonorm.py b/pvlib/iotools/meteonorm.py
index 08a94ea2a5..33a4e4dc9d 100644
--- a/pvlib/iotools/meteonorm.py
+++ b/pvlib/iotools/meteonorm.py
@@ -1,4 +1,4 @@
-"""Functions for reading and retrieving data from Meteonorm."""
+"""Functions for retrieving data from Meteonorm."""
import pandas as pd
import requests
@@ -38,7 +38,7 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
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 both historical and forecast data, but not TMY.
+ This function supports historical and forecast data, but not TMY.
Parameters
----------
@@ -57,35 +57,35 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
endpoint : str
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' - forcasts with hourly resolution
- * '/forecast/precision' - forecsat with 15-min resolution
+ * ``'/observation/training'`` - historical data with a 7-day delay
+ * ``'/observation/realtime'`` - near-real time (past 7-days)
+ * ``'/forecast/basic'`` - forcast with hourly resolution
+ * ``'/forecast/precision'`` - forecast with 15-min resolution
parameters : list, optional
List of parameters to request or 'all' to get all parameters. The
- default is "all".
- surface_tilt: float, default: 0
- Tilt angle from horizontal plane.
- surface_azimuth: float, default: 180
+ default is 'all'.
+ surface_tilt: float, optional
+ Tilt angle from horizontal plane. The default is 0.
+ surface_azimuth: float, optional
Orientation (azimuth angle) of the (fixed) plane. Clockwise from north
- (north=0, east=90, south=180, west=270).
+ (north=0, east=90, south=180, west=270). The default is 180.
time_step : {'1min', '15min', '1h'}, optional
- Frequency of the time series. The default is '15min'. The parameter is
- ignored if forcasting data is requested.
+ Frequency of the time series. The parameter is ignored when requesting
+ forcasting data. The default is '15min'.
horizon : optional
- Specification of the horizon line. Can be either a flat, 'auto', or
+ Specification of the horizon line. Can be either a 'flat', 'auto', or
a list of 360 horizon elevation angles. The default is 'auto'.
interval_index: bool, optional
Whether the index of the returned data object is of the type
pd.DatetimeIndex or pd.IntervalIndex. This is an experimental feature
which may be removed without warning. The default is False.
- map_variables: bool, default: True
+ map_variables: bool, optional
When true, renames columns of the Dataframe to pvlib variable names
where applicable. The default is True. See variable
:const:`VARIABLE_MAP`.
url: str, optional
- Base url of the Meteonorm API. The ``endpoint`` parameter is
+ Base URL of the Meteonorm API. The ``endpoint`` parameter is
appended to the url. The default is
:const:`pvlib.iotools.meteonorm.URL`.
@@ -98,7 +98,7 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
-------
data : pd.DataFrame
Time series data. The index corresponds to the start (left) of the
- interval.
+ interval unless ``interval_index`` is set to False.
meta : dict
Metadata.
@@ -125,15 +125,12 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
'lon': longitude,
'start': start.strftime('%Y-%m-%dT%H:%M:%SZ'),
'end': end.strftime('%Y-%m-%dT%H:%M:%SZ'),
+ 'parameters': parameters,
'surface_tilt': surface_tilt,
'surface_azimuth': surface_azimuth,
'horizon': horizon,
- 'parameters': parameters,
}
- if 'forecast' not in endpoint.lower():
- params['frequency'] = time_step_map.get(time_step, time_step)
-
# convert list to string with values separated by commas
if not isinstance(params['parameters'], (str, type(None))):
# allow the use of pvlib parameter names
@@ -141,41 +138,27 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
parameters = [parameter_dict.get(p, p) for p in parameters]
params['parameters'] = ','.join(parameters)
+ if horizon not in ['auto', 'flat']:
+ params['horizon'] = ','.join(horizon)
+
+ if 'forecast' not in endpoint.lower():
+ params['frequency'] = time_step_map.get(time_step, time_step)
+
headers = {"Authorization": f"Bearer {api_key}"}
response = requests.get(
urljoin(url, endpoint), headers=headers, params=params)
-
+ print(response)
if not response.ok:
# response.raise_for_status() does not give a useful error message
raise requests.HTTPError(response.json())
- data_json = response.json()['values']
- # identify empty columns
- empty_columns = [k for k, v in data_json.items() if v is None]
- # remove empty columns
- _ = [data_json.pop(k) for k in empty_columns]
-
- data = pd.DataFrame(data_json)
+ data, meta = _parse_meteonorm(response, interval_index, map_variables)
- # xxx: experimental feature - see parameter description
- if interval_index:
- data.index = pd.IntervalIndex.from_arrays(
- left=pd.to_datetime(response.json()['start_times']),
- right=pd.to_datetime(response.json()['end_times']),
- closed='both',
- )
- else:
- data.index = pd.to_datetime(response.json()['start_times'])
-
- meta = response.json()['meta']
+ return data, meta
- if map_variables:
- data = data.rename(columns=VARIABLE_MAP)
- meta['latitude'] = meta.pop('lat')
- meta['longitude'] = meta.pop('lon')
- return data, meta
+TMY_ENDPOINT = 'climate/tmy'
def get_meteonorm_tmy(latitude, longitude, api_key,
@@ -187,15 +170,11 @@ def get_meteonorm_tmy(latitude, longitude, api_key,
future_year=None, interval_index=False,
map_variables=True, url=URL):
"""
- Retrieve irradiance and weather data from Meteonorm.
+ Retrieve TMY irradiance and weather 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 the endpoints 'realtime' for data for the past 7
- days, 'training' for historical data with a delay of 7 days. The function
- does not support TMY climate data.
-
Parameters
----------
latitude: float
@@ -207,11 +186,11 @@ def get_meteonorm_tmy(latitude, longitude, api_key,
parameters: list, optional
List of parameters to request or 'all' to get all parameters. The
default is 'all'.
- surface_tilt: float, default: 0
- Tilt angle from horizontal plane.
- surface_azimuth : float, default: 180
+ surface_tilt: float, optional
+ Tilt angle from horizontal plane. The default is 0.
+ surface_azimuth : float, optional
Orientation (azimuth angle) of the (fixed) plane. Clockwise from north
- (north=0, east=90, south=180, west=270).
+ (north=0, east=90, south=180, west=270). The default is 180.
time_step: {'1min', '1h'}, optional
Frequency of the time series. The default is '1h'.
horizon: optional
@@ -244,13 +223,13 @@ def get_meteonorm_tmy(latitude, longitude, api_key,
Whether the index of the returned data object is of the type
pd.DatetimeIndex or pd.IntervalIndex. This is an experimental feature
which may be removed without warning. The default is False.
- map_variables: bool, default: True
+ map_variables: bool, optional
When true, renames columns of the Dataframe to pvlib variable names
- where applicable. The default is True. See variable
- :const:`VARIABLE_MAP`.
+ where applicable. See variable :const:`VARIABLE_MAP`. The default is
+ True.
url: str, optional.
- Base url of the Meteonorm API. 'climate/tmy'` is
- appended to the url. The default is:
+ Base URL of the Meteonorm API. 'climate/tmy'` is
+ appended to the URL. The default is:
:const:`pvlib.iotools.meteonorm.URL`.
Raises
@@ -262,7 +241,7 @@ def get_meteonorm_tmy(latitude, longitude, api_key,
-------
data : pd.DataFrame
Time series data. The index corresponds to the start (left) of the
- interval.
+ interval unless ``interval_index`` is set to False.
meta : dict
Metadata.
@@ -293,6 +272,16 @@ def get_meteonorm_tmy(latitude, longitude, api_key,
'data_version': data_version,
}
+ # convert list to string with values separated by commas
+ if not isinstance(params['parameters'], (str, type(None))):
+ # 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['parameters'] = ','.join(parameters)
+
+ if horizon not in ['auto', 'flat']:
+ params['horizon'] = ','.join(horizon)
+
if turbidity != 'auto':
params['turbidity'] = ','.join(turbidity)
@@ -305,24 +294,21 @@ def get_meteonorm_tmy(latitude, longitude, api_key,
if future_year is not None:
params['future_year'] = future_year
- # convert list to string with values separated by commas
- if not isinstance(params['parameters'], (str, type(None))):
- # 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['parameters'] = ','.join(parameters)
-
headers = {"Authorization": f"Bearer {api_key}"}
- endpoint = 'climate/tmy'
-
response = requests.get(
- urljoin(url, endpoint), headers=headers, params=params)
+ urljoin(url, TMY_ENDPOINT), headers=headers, params=params)
if not response.ok:
# response.raise_for_status() does not give a useful error message
raise requests.HTTPError(response.json())
+ data, meta = _parse_meteonorm(response, interval_index, map_variables)
+
+ return data, meta
+
+
+def _parse_meteonorm(response, interval_index, map_variables):
data_json = response.json()['values']
# identify empty columns
empty_columns = [k for k, v in data_json.items() if v is None]
From 5c3c9bf65e0c9ee8a567178e3018150e479b0e8c Mon Sep 17 00:00:00 2001
From: "Adam R. Jensen" <39184289+AdamRJensen@users.noreply.github.com>
Date: Sat, 2 Aug 2025 15:50:23 +0200
Subject: [PATCH 04/18] Apply suggestions from code review
Co-authored-by: Ioannis Sifnaios <88548539+IoannisSifnaios@users.noreply.github.com>
---
pvlib/iotools/meteonorm.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/pvlib/iotools/meteonorm.py b/pvlib/iotools/meteonorm.py
index 33a4e4dc9d..df5106208a 100644
--- a/pvlib/iotools/meteonorm.py
+++ b/pvlib/iotools/meteonorm.py
@@ -212,12 +212,12 @@ def get_meteonorm_tmy(latitude, longitude, api_key,
with the same random seed will yield identical results.
clear_sky_radiation_model : {'esra', 'solis'}
Which clearsky model to use. The default is 'esra'.
- data_version : string, optional
+ data_version : str, optional
Version of Meteonorm climatological data to be used. The default is
'latest'.
- future_scenario: string, optional
+ future_scenario: str, optional
Future climate scenario.
- future_year : integer, optional
+ future_year : int, optional
Central year for a 20-year reference period in the future.
interval_index: bool, optional
Whether the index of the returned data object is of the type
@@ -228,7 +228,7 @@ def get_meteonorm_tmy(latitude, longitude, api_key,
where applicable. See variable :const:`VARIABLE_MAP`. The default is
True.
url: str, optional.
- Base URL of the Meteonorm API. 'climate/tmy'` is
+ Base URL of the Meteonorm API. `'climate/tmy'` is
appended to the URL. The default is:
:const:`pvlib.iotools.meteonorm.URL`.
From 4a0e1b3dd469fac84ff28aa6e069101e5db3a10e Mon Sep 17 00:00:00 2001
From: "Adam R. Jensen" <39184289+AdamRJensen@users.noreply.github.com>
Date: Sat, 2 Aug 2025 15:52:41 +0200
Subject: [PATCH 05/18] Apply suggestions from code review
Co-authored-by: Ioannis Sifnaios <88548539+IoannisSifnaios@users.noreply.github.com>
---
pvlib/iotools/meteonorm.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/pvlib/iotools/meteonorm.py b/pvlib/iotools/meteonorm.py
index df5106208a..1c1446a98d 100644
--- a/pvlib/iotools/meteonorm.py
+++ b/pvlib/iotools/meteonorm.py
@@ -73,7 +73,7 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
time_step : {'1min', '15min', '1h'}, optional
Frequency of the time series. The parameter is ignored when requesting
forcasting data. The default is '15min'.
- horizon : optional
+ horizon : str, optional
Specification of the horizon line. Can be either a 'flat', 'auto', or
a list of 360 horizon elevation angles. The default is 'auto'.
interval_index: bool, optional
@@ -193,11 +193,11 @@ def get_meteonorm_tmy(latitude, longitude, api_key,
(north=0, east=90, south=180, west=270). The default is 180.
time_step: {'1min', '1h'}, optional
Frequency of the time series. The default is '1h'.
- horizon: optional
+ horizon: str, optional
Specification of the hoirzon line. Can be either 'flat' or 'auto', or
specified as a list of 360 horizon elevation angles. The default is
'auto'.
- terrain: string, optional
+ terrain: str, optional
Local terrain situation. Must be one of: ['open', 'depression',
'cold_air_lake', 'sea_lake', 'city', 'slope_south',
'slope_west_east']. The default is 'open'.
From 10618a28597ab6093a24d75052ffddc28608b47f Mon Sep 17 00:00:00 2001
From: "Adam R. Jensen" <39184289+AdamRJensen@users.noreply.github.com>
Date: Sat, 2 Aug 2025 19:20:02 +0200
Subject: [PATCH 06/18] Improve docstring
---
pvlib/iotools/meteonorm.py | 92 ++++++++++++++++++--------------------
1 file changed, 43 insertions(+), 49 deletions(-)
diff --git a/pvlib/iotools/meteonorm.py b/pvlib/iotools/meteonorm.py
index 1c1446a98d..ce5a91bb62 100644
--- a/pvlib/iotools/meteonorm.py
+++ b/pvlib/iotools/meteonorm.py
@@ -19,7 +19,7 @@
'dew_point_temperature': 'temp_dew',
}
-time_step_map = {
+TIME_STEP_MAP = {
'1h': '1_hour',
'h': '1_hour',
'15min': '15_minutes',
@@ -38,7 +38,8 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
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 historical and forecast data, but not TMY.
+ This function supports retrieval of historical and forecast data, but not
+ TMY.
Parameters
----------
@@ -46,12 +47,12 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
In decimal degrees, north is positive (ISO 19115).
longitude: float
In decimal degrees, east is positive (ISO 19115).
- start: datetime like, optional
+ start: datetime like
First timestamp of the requested period. If a timezone is not
- specified, UTC is assumed. A relative datetime string is also allowed.
- end: datetime like, optional
+ specified, UTC is assumed. Relative datetime strings are supported.
+ end: datetime like
Last timestamp of the requested period. If a timezone is not
- specified, UTC is assumed. A relative datetime string is also allowed.
+ specified, UTC is assumed. Relative datetime strings are supported.
api_key: str
Meteonorm API key.
endpoint : str
@@ -62,28 +63,26 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
* ``'/forecast/basic'`` - forcast with hourly resolution
* ``'/forecast/precision'`` - forecast with 15-min resolution
- parameters : list, optional
- List of parameters to request or 'all' to get all parameters. The
- default is 'all'.
- surface_tilt: float, optional
- Tilt angle from horizontal plane. The default is 0.
- surface_azimuth: float, optional
+ 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). The default is 180.
- time_step : {'1min', '15min', '1h'}, optional
+ (north=0, east=90, south=180, west=270).
+ time_step : {'1min', '15min', '1h'}, default : '15min'
Frequency of the time series. The parameter is ignored when requesting
- forcasting data. The default is '15min'.
- horizon : str, optional
+ forcasting data.
+ horizon : str or list, default : 'auto'
Specification of the horizon line. Can be either a 'flat', 'auto', or
- a list of 360 horizon elevation angles. The default is 'auto'.
- interval_index: bool, optional
+ a list of 360 horizon elevation angles.
+ interval_index: bool, default : False
Whether the index of the returned data object is of the type
pd.DatetimeIndex or pd.IntervalIndex. This is an experimental feature
- which may be removed without warning. The default is False.
- map_variables: bool, optional
+ which may be removed without warning.
+ map_variables: bool, default : True
When true, renames columns of the Dataframe to pvlib variable names
- where applicable. The default is True. See variable
- :const:`VARIABLE_MAP`.
+ 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
@@ -142,13 +141,12 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
params['horizon'] = ','.join(horizon)
if 'forecast' not in endpoint.lower():
- params['frequency'] = time_step_map.get(time_step, time_step)
+ params['frequency'] = TIME_STEP_MAP.get(time_step, time_step)
headers = {"Authorization": f"Bearer {api_key}"}
response = requests.get(
urljoin(url, endpoint), headers=headers, params=params)
- print(response)
if not response.ok:
# response.raise_for_status() does not give a useful error message
raise requests.HTTPError(response.json())
@@ -183,50 +181,46 @@ def get_meteonorm_tmy(latitude, longitude, api_key,
In decimal degrees, east is positive (ISO 19115).
api_key: str
Meteonorm API key.
- parameters: list, optional
- List of parameters to request or 'all' to get all parameters. The
- default is 'all'.
- surface_tilt: float, optional
- Tilt angle from horizontal plane. The default is 0.
- surface_azimuth : float, optional
+ 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). The default is 180.
- time_step: {'1min', '1h'}, optional
- Frequency of the time series. The default is '1h'.
+ (north=0, east=90, south=180, west=270).
+ time_step: {'1min', '1h'}, default : '1h'
+ Frequency of the time series.
horizon: str, optional
Specification of the hoirzon line. Can be either 'flat' or 'auto', or
- specified as a list of 360 horizon elevation angles. The default is
+ specified as a list of 360 horizon elevation angles.
'auto'.
- terrain: str, optional
+ terrain: str, default : 'open'
Local terrain situation. Must be one of: ['open', 'depression',
'cold_air_lake', 'sea_lake', 'city', 'slope_south',
- 'slope_west_east']. The default is 'open'.
- albedo: float, optional
- Ground albedo. Albedo changes due to snow fall are modelled. The
- default is 0.2.
+ 'slope_west_east'].
+ albedo: float, default : 0.2
+ Ground albedo. Albedo changes due to snow fall are modelled.
turbidity: list or 'auto', optional
List of 12 monthly mean atmospheric Linke turbidity values. The default
is 'auto'.
random_seed: int, optional
Random seed to be used for stochastic processes. Two identical requests
with the same random seed will yield identical results.
- clear_sky_radiation_model : {'esra', 'solis'}
- Which clearsky model to use. The default is 'esra'.
- data_version : str, optional
- Version of Meteonorm climatological data to be used. The default is
- 'latest'.
+ clear_sky_radiation_model : str, default : 'esra'
+ Which clearsky model to use. Must be either `'esra'` or `'solis'`.
+ data_version : str, default : 'latest'
+ Version of Meteonorm climatological data to be used.
future_scenario: str, optional
Future climate scenario.
future_year : int, optional
Central year for a 20-year reference period in the future.
- interval_index: bool, optional
+ interval_index: bool, default : False
Whether the index of the returned data object is of the type
pd.DatetimeIndex or pd.IntervalIndex. This is an experimental feature
- which may be removed without warning. The default is False.
- map_variables: bool, optional
+ 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`. The default is
- True.
+ where applicable. See variable :const:`VARIABLE_MAP`.
url: str, optional.
Base URL of the Meteonorm API. `'climate/tmy'` is
appended to the URL. The default is:
From a42e68f14604a4a1482216e2395746711c7407b5 Mon Sep 17 00:00:00 2001
From: "Adam R. Jensen" <39184289+AdamRJensen@users.noreply.github.com>
Date: Sat, 2 Aug 2025 20:44:19 +0200
Subject: [PATCH 07/18] Improve functions
---
pvlib/iotools/meteonorm.py | 99 ++++++++++++++++++--------------------
1 file changed, 46 insertions(+), 53 deletions(-)
diff --git a/pvlib/iotools/meteonorm.py b/pvlib/iotools/meteonorm.py
index ce5a91bb62..1514831eb1 100644
--- a/pvlib/iotools/meteonorm.py
+++ b/pvlib/iotools/meteonorm.py
@@ -43,31 +43,31 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
Parameters
----------
- latitude: float
+ latitude : float
In decimal degrees, north is positive (ISO 19115).
longitude: float
In decimal degrees, east is positive (ISO 19115).
- start: datetime like
+ start : datetime like
First timestamp of the requested period. If a timezone is not
specified, UTC is assumed. Relative datetime strings are supported.
- end: datetime like
+ end : datetime like
Last timestamp of the requested period. If a timezone is not
specified, UTC is assumed. Relative datetime strings are supported.
- api_key: str
+ api_key : str
Meteonorm API key.
endpoint : str
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'`` - forcast with hourly resolution
- * ``'/forecast/precision'`` - forecast with 15-min resolution
+ * ``'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 15-min resolution
- parameters: list or 'all', default 'all'
+ parameters : list or 'all', default : 'all'
List of parameters to request or `'all'` to get all parameters.
- surface_tilt: float, default : 0
+ surface_tilt : float, default : 0
Tilt angle from horizontal plane.
- surface_azimuth: float, default : 180
+ 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'
@@ -76,14 +76,13 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
horizon : str or list, default : 'auto'
Specification of the horizon line. Can be either a 'flat', 'auto', or
a list of 360 horizon elevation angles.
- interval_index: bool, default : False
- Whether the index of the returned data object is of the type
- pd.DatetimeIndex or pd.IntervalIndex. This is an experimental feature
- which may be removed without warning.
- map_variables: bool, default : True
+ 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
+ url : str, optional
Base URL of the Meteonorm API. The ``endpoint`` parameter is
appended to the url. The default is
:const:`pvlib.iotools.meteonorm.URL`.
@@ -137,8 +136,8 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
parameters = [parameter_dict.get(p, p) for p in parameters]
params['parameters'] = ','.join(parameters)
- if horizon not in ['auto', 'flat']:
- params['horizon'] = ','.join(horizon)
+ if not isinstance(horizon, str):
+ params['horizon'] = ','.join(map(str, horizon))
if 'forecast' not in endpoint.lower():
params['frequency'] = TIME_STEP_MAP.get(time_step, time_step)
@@ -146,7 +145,8 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
headers = {"Authorization": f"Bearer {api_key}"}
response = requests.get(
- urljoin(url, endpoint), headers=headers, params=params)
+ urljoin(url, endpoint.lstrip('/')), headers=headers, params=params)
+
if not response.ok:
# response.raise_for_status() does not give a useful error message
raise requests.HTTPError(response.json())
@@ -175,53 +175,52 @@ def get_meteonorm_tmy(latitude, longitude, api_key,
Parameters
----------
- latitude: float
+ latitude : float
In decimal degrees, north is positive (ISO 19115).
- longitude: float
+ longitude : float
In decimal degrees, east is positive (ISO 19115).
- api_key: str
+ api_key : str
Meteonorm API key.
- parameters: list or 'all', default 'all'
+ parameters : list or 'all', default : 'all'
List of parameters to request or `'all'` to get all parameters.
- surface_tilt: float, default : 0
+ 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', '1h'}, default : '1h'
+ time_step : {'1min', '1h'}, default : '1h'
Frequency of the time series.
- horizon: str, optional
- Specification of the hoirzon line. Can be either 'flat' or 'auto', or
+ horizon : str, optional
+ Specification of the horizon line. Can be either 'flat' or 'auto', or
specified as a list of 360 horizon elevation angles.
'auto'.
- terrain: str, default : 'open'
+ terrain : str, default : 'open'
Local terrain situation. Must be one of: ['open', 'depression',
'cold_air_lake', 'sea_lake', 'city', 'slope_south',
'slope_west_east'].
- albedo: float, default : 0.2
+ albedo : float, default : 0.2
Ground albedo. Albedo changes due to snow fall are modelled.
- turbidity: list or 'auto', optional
+ turbidity : list or 'auto', optional
List of 12 monthly mean atmospheric Linke turbidity values. The default
is 'auto'.
- random_seed: int, optional
+ random_seed : int, optional
Random seed to be used for stochastic processes. Two identical requests
with the same random seed will yield identical results.
clear_sky_radiation_model : str, default : 'esra'
Which clearsky model to use. Must be either `'esra'` or `'solis'`.
data_version : str, default : 'latest'
Version of Meteonorm climatological data to be used.
- future_scenario: str, optional
+ future_scenario : str, optional
Future climate scenario.
future_year : int, optional
Central year for a 20-year reference period in the future.
- interval_index: bool, default : False
- Whether the index of the returned data object is of the type
- pd.DatetimeIndex or pd.IntervalIndex. This is an experimental feature
- which may be removed without warning.
- map_variables: bool, default : True
+ 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.
+ url : str, optional.
Base URL of the Meteonorm API. `'climate/tmy'` is
appended to the URL. The default is:
:const:`pvlib.iotools.meteonorm.URL`.
@@ -264,6 +263,9 @@ def get_meteonorm_tmy(latitude, longitude, api_key,
'turbidity': turbidity,
'clear_sky_radiation_model': clear_sky_radiation_model,
'data_version': data_version,
+ 'random_seed': random_seed,
+ 'future_scenario': future_scenario,
+ 'future_year': future_year,
}
# convert list to string with values separated by commas
@@ -273,25 +275,16 @@ def get_meteonorm_tmy(latitude, longitude, api_key,
parameters = [parameter_dict.get(p, p) for p in parameters]
params['parameters'] = ','.join(parameters)
- if horizon not in ['auto', 'flat']:
- params['horizon'] = ','.join(horizon)
-
- if turbidity != 'auto':
- params['turbidity'] = ','.join(turbidity)
-
- if random_seed is not None:
- params['random_seed'] = random_seed
-
- if future_scenario is not None:
- params['future_scenario'] = future_scenario
+ if isinstance(horizon, str):
+ params['horizon'] = ','.join(map(str, horizon))
- if future_year is not None:
- params['future_year'] = future_year
+ if isinstance(turbidity, str):
+ params['turbidity'] = ','.join(map(str, turbidity))
headers = {"Authorization": f"Bearer {api_key}"}
response = requests.get(
- urljoin(url, TMY_ENDPOINT), headers=headers, params=params)
+ urljoin(url, TMY_ENDPOINT.lstrip('/')), headers=headers, params=params)
if not response.ok:
# response.raise_for_status() does not give a useful error message
From acbdafac3eda01b34423291f92333ae029619ab3 Mon Sep 17 00:00:00 2001
From: "Adam R. Jensen" <39184289+AdamRJensen@users.noreply.github.com>
Date: Sat, 2 Aug 2025 20:44:27 +0200
Subject: [PATCH 08/18] Add first round of tests
---
tests/iotools/test_meteonorm.py | 213 ++++++++++++++++++++++++++++++++
1 file changed, 213 insertions(+)
create mode 100644 tests/iotools/test_meteonorm.py
diff --git a/tests/iotools/test_meteonorm.py b/tests/iotools/test_meteonorm.py
new file mode 100644
index 0000000000..0735b0f5be
--- /dev/null
+++ b/tests/iotools/test_meteonorm.py
@@ -0,0 +1,213 @@
+import pandas as pd
+import numpy as np
+import pytest
+import pvlib
+from tests.conftest import RERUNS, RERUNS_DELAY
+
+
+@pytest.fixture
+def demo_api_key():
+ # Demo locations:
+ # lat=50, lon=10 (Germany)
+ # lat=21, lon=79 (India)
+ # lat=-3, lon=-60 (Brazil)
+ # lat=51, lon=-114 (Canada)
+ # lat=24, lon=33 (Egypt)
+ return 'demo0000-0000-0000-0000-000000000000'
+
+
+@pytest.fixture
+def demo_url():
+ return 'https://demo.meteonorm.com/v1/'
+
+
+@pytest.fixture
+def expected_meta():
+ meta = {
+ 'altitude': 290,
+ 'frequency': '1_hour',
+ 'parameters': [
+ {'aggregation_method': 'average',
+ 'description': 'Diffuse horizontal irradiance',
+ 'name': 'diffuse_horizontal_irradiance',
+ 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
+ {'aggregation_method': 'average',
+ 'description': 'Diffuse horizontal irradiance with shading taken into account',
+ 'name': 'diffuse_horizontal_irradiance_with_shading',
+ 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
+ {'aggregation_method': 'average',
+ 'description': 'Diffuse tilted irradiance',
+ 'name': 'diffuse_tilted_irradiance',
+ 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
+ {'aggregation_method': 'average',
+ 'description': 'Diffuse tilted irradiance with shading taken into account',
+ 'name': 'diffuse_tilted_irradiance_with_shading',
+ 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
+ {'aggregation_method': 'average',
+ 'description': 'Direct horizontal irradiance',
+ 'name': 'direct_horizontal_irradiance',
+ 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
+ {'aggregation_method': 'average',
+ 'description': 'Direct horizontal irradiance with shading taken into account',
+ 'name': 'direct_horizontal_irradiance_with_shading',
+ 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
+ {'aggregation_method': 'average',
+ 'description': 'Direct normal irradiance',
+ 'name': 'direct_normal_irradiance',
+ 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
+ {'aggregation_method': 'average',
+ 'description': 'Direct normal irradiance with shading taken into account',
+ 'name': 'direct_normal_irradiance_with_shading',
+ 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
+ {'aggregation_method': 'average',
+ 'description': 'Direct tilted irradiance',
+ 'name': 'direct_tilted_irradiance',
+ 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
+ {'aggregation_method': 'average',
+ 'description': 'Direct tilted irradiance with shading taken into account',
+ 'name': 'direct_tilted_irradiance_with_shading',
+ 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
+ {'aggregation_method': 'average',
+ 'description': 'Global horizontal clear sky irradiance',
+ 'name': 'global_clear_sky_irradiance',
+ 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
+ {'aggregation_method': 'average',
+ 'description': 'Global horizontal irradiance',
+ 'name': 'global_horizontal_irradiance',
+ 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
+ {'aggregation_method': 'average',
+ 'description': 'Global horizontal irradiance with shading taken into account',
+ 'name': 'global_horizontal_irradiance_with_shading',
+ 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
+ {'aggregation_method': 'average',
+ 'description': 'Global tilted irradiance',
+ 'name': 'global_tilted_irradiance',
+ 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
+ {'aggregation_method': 'average',
+ 'description': 'Global tilted irradiance with shading taken into account',
+ 'name': 'global_tilted_irradiance_with_shading',
+ 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
+ {'aggregation_method': 'average',
+ 'description': 'Power output per kWp installed',
+ 'name': 'pv_production',
+ 'unit': {'description': 'Watts per kilowatt peak', 'name': 'W/kWp'}},
+ {'aggregation_method': 'average',
+ 'description': 'Power output per kWp installed, with shading taken into account',
+ 'name': 'pv_production_with_shading',
+ 'unit': {'description': 'Watts per kilowatt peak', 'name': 'W/kWp'}},
+ {'aggregation_method': 'average',
+ 'description': 'Snow depth',
+ 'name': 'snow_depth',
+ 'unit': {'description': 'millimeters', 'name': 'mm'}},
+ {'aggregation_method': 'average',
+ 'description': 'Air temperature, 2 m above ground.',
+ 'name': 'temperature',
+ 'unit': {'description': 'degrees Celsius', 'name': '°C'}}],
+ 'surface_azimuth': 180,
+ 'surface_tilt': 0,
+ 'time_zone': 0,
+ 'latitude': 50,
+ 'longitude': 10,
+ }
+ return meta
+
+
+@pytest.fixture
+def expected_meteonorm_index():
+ expected_meteonorm_index = \
+ pd.date_range('2023-01-01', '2024-12-31 23:59', freq='1h', tz='UTC')
+ expected_meteonorm_index.freq = None
+ return expected_meteonorm_index
+
+
+@pytest.fixture
+def expected_metenorm_data():
+ # The first 12 rows of data
+ columns = ['dhi', 'diffuse_horizontal_irradiance_with_shading', 'poa_diffuse',
+ 'diffuse_tilted_irradiance_with_shading', 'bhi',
+ 'direct_horizontal_irradiance_with_shading', 'dni',
+ 'direct_normal_irradiance_with_shading', 'poa_direct',
+ 'direct_tilted_irradiance_with_shading', 'ghi_clear', 'ghi',
+ 'global_horizontal_irradiance_with_shading', 'poa',
+ 'global_tilted_irradiance_with_shading', 'pv_production',
+ 'pv_production_with_shading', 'snow_depth', 'temp_air']
+ expected = [
+ [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 12.25],
+ [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 11.75],
+ [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 11.75],
+ [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 11.5],
+ [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 11.25],
+ [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 11.],
+ [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 11.],
+ [2.5, 2.68309898, 2.67538201, 2.68309898, 0., 0., 0., 0., 0., 0., 0., 2.5,
+ 2.68309898, 2.67538201, 2.68309898, 2.34649978, 2.35326557, 0., 11.],
+ [40.43632435, 40.41304027, 40.43632435, 40.41304027, 37.06367565, 37.06367565,
+ 288.7781947, 288.7781947, 37.06367565, 37.06367565, 98.10113439, 77.5,
+ 77.47671591, 77.5, 77.47671591, 67.02141875, 67.00150474, 0., 11.75],
+ [60.52591348, 60.51498257, 60.52591348, 60.51498257, 104.47408652, 104.47408652,
+ 478.10101591, 478.10101591, 104.47408652, 104.47408652, 191.27910925, 165.,
+ 164.98906908, 165., 164.98906908, 140.23845, 140.22938131, 0., 12.75],
+ [71.90169306, 71.89757085, 71.90169306, 71.89757085, 138.84830694, 138.84830694,
+ 508.02986044, 508.02986044, 138.84830694, 138.84830694, 253.85597777, 210.75,
+ 210.7458778, 210.75, 210.7458778, 177.07272956, 177.06937293, 0., 13.75],
+ [78.20403711, 78.19681926, 78.20403711, 78.19681926, 142.79596289, 142.79596289,
+ 494.06576548, 494.06576548, 142.79596289, 142.79596289, 272.34275335, 221.,
+ 220.99278214, 221., 220.99278214, 185.179657, 185.17380523, 0., 14.],
+ ]
+ index = pd.date_range('2023-01-01', periods=12, freq='1h', tz='UTC')
+ index.freq = None
+ expected = pd.DataFrame(expected, index=index, columns=columns)
+ expected['snow_depth'] = expected['snow_depth'].astype(np.int64)
+ return expected
+
+
+@pytest.mark.remote_data
+@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
+def test_get_meteonorm_training(
+ demo_api_key, demo_url, expected_meta, expected_meteonorm_index,
+ expected_metenorm_data):
+ data, meta = pvlib.iotools.get_meteonorm(
+ latitude=50, longitude=10,
+ start='2023-01-01', end='2025-01-01',
+ api_key=demo_api_key,
+ endpoint='observation/training',
+ time_step='1h',
+ url=demo_url)
+
+ assert meta == expected_meta
+ pd.testing.assert_index_equal(data.index, expected_meteonorm_index)
+ pd.testing.assert_frame_equal(data.iloc[:12], expected_metenorm_data)
+
+
+@pytest.mark.remote_data
+@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
+def test_get_meteonorm_realtime(
+ demo_api_key, demo_url, expected_meta, expected_meteonorm_index,
+ expected_metenorm_data):
+ data, meta = pvlib.iotools.get_meteonorm(
+ 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=['ghi', 'global_horizontal_irradiance_with_shading'],
+ api_key=demo_api_key,
+ endpoint='/observation/realtime',
+ time_step='1min',
+ horizon='flat',
+ map_variables=False,
+ interval_index=True,
+ url=demo_url,
+ )
+ assert meta['frequency'] == '1_minute'
+ assert meta['lat'] == 21
+ assert meta['lon'] == 79
+ assert meta['surface_tilt'] == 20
+ assert meta['surface_azimuth'] == 10
+
+ assert all(data.columns == pd.Index([
+ 'global_horizontal_irradiance',
+ 'global_horizontal_irradiance_with_shading']))
+ assert data.shape == (241, 2)
+ # can't test the specific index as it varies due to the
+ # use of pd.Timestamp.now
+ assert type(data.index) is pd.core.indexes.interval.IntervalIndex
From ef4f83865055344f1db969a2aa0b14d94fdda034 Mon Sep 17 00:00:00 2001
From: "Adam R. Jensen" <39184289+AdamRJensen@users.noreply.github.com>
Date: Sat, 2 Aug 2025 21:10:16 +0200
Subject: [PATCH 09/18] Update tests
---
pvlib/iotools/meteonorm.py | 16 ++-
tests/iotools/test_meteonorm.py | 189 ++++++++++++++------------------
2 files changed, 96 insertions(+), 109 deletions(-)
diff --git a/pvlib/iotools/meteonorm.py b/pvlib/iotools/meteonorm.py
index 1514831eb1..d58ce6e894 100644
--- a/pvlib/iotools/meteonorm.py
+++ b/pvlib/iotools/meteonorm.py
@@ -129,8 +129,14 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
'horizon': horizon,
}
+ # Allow specifying single parameters as string
+ if isinstance(parameters, str):
+ parameter_list = list(VARIABLE_MAP.keys()) + list(VARIABLE_MAP.values())
+ if parameters in parameter_list:
+ parameters = [parameters]
+
# convert list to string with values separated by commas
- if not isinstance(params['parameters'], (str, type(None))):
+ if not isinstance(parameters, (str, type(None))):
# 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]
@@ -268,8 +274,14 @@ def get_meteonorm_tmy(latitude, longitude, api_key,
'future_year': future_year,
}
+ # Allow specifying single parameters as string
+ if isinstance(parameters, str):
+ parameter_list = list(VARIABLE_MAP.keys()) + list(VARIABLE_MAP.values())
+ if parameters in parameter_list:
+ parameters = [parameters]
+
# convert list to string with values separated by commas
- if not isinstance(params['parameters'], (str, type(None))):
+ if not isinstance(parameters, (str, type(None))):
# 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]
diff --git a/tests/iotools/test_meteonorm.py b/tests/iotools/test_meteonorm.py
index 0735b0f5be..e809d2a41f 100644
--- a/tests/iotools/test_meteonorm.py
+++ b/tests/iotools/test_meteonorm.py
@@ -1,5 +1,4 @@
import pandas as pd
-import numpy as np
import pytest
import pvlib
from tests.conftest import RERUNS, RERUNS_DELAY
@@ -27,50 +26,6 @@ def expected_meta():
'altitude': 290,
'frequency': '1_hour',
'parameters': [
- {'aggregation_method': 'average',
- 'description': 'Diffuse horizontal irradiance',
- 'name': 'diffuse_horizontal_irradiance',
- 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
- {'aggregation_method': 'average',
- 'description': 'Diffuse horizontal irradiance with shading taken into account',
- 'name': 'diffuse_horizontal_irradiance_with_shading',
- 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
- {'aggregation_method': 'average',
- 'description': 'Diffuse tilted irradiance',
- 'name': 'diffuse_tilted_irradiance',
- 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
- {'aggregation_method': 'average',
- 'description': 'Diffuse tilted irradiance with shading taken into account',
- 'name': 'diffuse_tilted_irradiance_with_shading',
- 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
- {'aggregation_method': 'average',
- 'description': 'Direct horizontal irradiance',
- 'name': 'direct_horizontal_irradiance',
- 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
- {'aggregation_method': 'average',
- 'description': 'Direct horizontal irradiance with shading taken into account',
- 'name': 'direct_horizontal_irradiance_with_shading',
- 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
- {'aggregation_method': 'average',
- 'description': 'Direct normal irradiance',
- 'name': 'direct_normal_irradiance',
- 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
- {'aggregation_method': 'average',
- 'description': 'Direct normal irradiance with shading taken into account',
- 'name': 'direct_normal_irradiance_with_shading',
- 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
- {'aggregation_method': 'average',
- 'description': 'Direct tilted irradiance',
- 'name': 'direct_tilted_irradiance',
- 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
- {'aggregation_method': 'average',
- 'description': 'Direct tilted irradiance with shading taken into account',
- 'name': 'direct_tilted_irradiance_with_shading',
- 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
- {'aggregation_method': 'average',
- 'description': 'Global horizontal clear sky irradiance',
- 'name': 'global_clear_sky_irradiance',
- 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
{'aggregation_method': 'average',
'description': 'Global horizontal irradiance',
'name': 'global_horizontal_irradiance',
@@ -79,30 +34,7 @@ def expected_meta():
'description': 'Global horizontal irradiance with shading taken into account',
'name': 'global_horizontal_irradiance_with_shading',
'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
- {'aggregation_method': 'average',
- 'description': 'Global tilted irradiance',
- 'name': 'global_tilted_irradiance',
- 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
- {'aggregation_method': 'average',
- 'description': 'Global tilted irradiance with shading taken into account',
- 'name': 'global_tilted_irradiance_with_shading',
- 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
- {'aggregation_method': 'average',
- 'description': 'Power output per kWp installed',
- 'name': 'pv_production',
- 'unit': {'description': 'Watts per kilowatt peak', 'name': 'W/kWp'}},
- {'aggregation_method': 'average',
- 'description': 'Power output per kWp installed, with shading taken into account',
- 'name': 'pv_production_with_shading',
- 'unit': {'description': 'Watts per kilowatt peak', 'name': 'W/kWp'}},
- {'aggregation_method': 'average',
- 'description': 'Snow depth',
- 'name': 'snow_depth',
- 'unit': {'description': 'millimeters', 'name': 'mm'}},
- {'aggregation_method': 'average',
- 'description': 'Air temperature, 2 m above ground.',
- 'name': 'temperature',
- 'unit': {'description': 'degrees Celsius', 'name': '°C'}}],
+ ],
'surface_azimuth': 180,
'surface_tilt': 0,
'time_zone': 0,
@@ -123,44 +55,53 @@ def expected_meteonorm_index():
@pytest.fixture
def expected_metenorm_data():
# The first 12 rows of data
- columns = ['dhi', 'diffuse_horizontal_irradiance_with_shading', 'poa_diffuse',
- 'diffuse_tilted_irradiance_with_shading', 'bhi',
- 'direct_horizontal_irradiance_with_shading', 'dni',
- 'direct_normal_irradiance_with_shading', 'poa_direct',
- 'direct_tilted_irradiance_with_shading', 'ghi_clear', 'ghi',
- 'global_horizontal_irradiance_with_shading', 'poa',
- 'global_tilted_irradiance_with_shading', 'pv_production',
- 'pv_production_with_shading', 'snow_depth', 'temp_air']
+ columns = ['ghi', 'global_horizontal_irradiance_with_shading']
expected = [
- [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 12.25],
- [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 11.75],
- [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 11.75],
- [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 11.5],
- [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 11.25],
- [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 11.],
- [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 11.],
- [2.5, 2.68309898, 2.67538201, 2.68309898, 0., 0., 0., 0., 0., 0., 0., 2.5,
- 2.68309898, 2.67538201, 2.68309898, 2.34649978, 2.35326557, 0., 11.],
- [40.43632435, 40.41304027, 40.43632435, 40.41304027, 37.06367565, 37.06367565,
- 288.7781947, 288.7781947, 37.06367565, 37.06367565, 98.10113439, 77.5,
- 77.47671591, 77.5, 77.47671591, 67.02141875, 67.00150474, 0., 11.75],
- [60.52591348, 60.51498257, 60.52591348, 60.51498257, 104.47408652, 104.47408652,
- 478.10101591, 478.10101591, 104.47408652, 104.47408652, 191.27910925, 165.,
- 164.98906908, 165., 164.98906908, 140.23845, 140.22938131, 0., 12.75],
- [71.90169306, 71.89757085, 71.90169306, 71.89757085, 138.84830694, 138.84830694,
- 508.02986044, 508.02986044, 138.84830694, 138.84830694, 253.85597777, 210.75,
- 210.7458778, 210.75, 210.7458778, 177.07272956, 177.06937293, 0., 13.75],
- [78.20403711, 78.19681926, 78.20403711, 78.19681926, 142.79596289, 142.79596289,
- 494.06576548, 494.06576548, 142.79596289, 142.79596289, 272.34275335, 221.,
- 220.99278214, 221., 220.99278214, 185.179657, 185.17380523, 0., 14.],
+ [0.0, 0.0],
+ [0.0, 0.0],
+ [0.0, 0.0],
+ [0.0, 0.0],
+ [0.0, 0.0],
+ [0.0, 0.0],
+ [0.0, 0.0],
+ [2.5, 2.68309898],
+ [77.5, 77.47671591],
+ [165.0, 164.98906908],
+ [210.75, 210.7458778],
+ [221.0, 220.99278214],
]
index = pd.date_range('2023-01-01', periods=12, freq='1h', tz='UTC')
index.freq = None
expected = pd.DataFrame(expected, index=index, columns=columns)
- expected['snow_depth'] = expected['snow_depth'].astype(np.int64)
return expected
+@pytest.fixture
+def expected_columns_all():
+ columns = [
+ 'diffuse_horizontal_irradiance',
+ 'diffuse_horizontal_irradiance_with_shading',
+ 'diffuse_tilted_irradiance',
+ 'diffuse_tilted_irradiance_with_shading',
+ 'direct_horizontal_irradiance',
+ 'direct_horizontal_irradiance_with_shading',
+ 'direct_normal_irradiance',
+ 'direct_normal_irradiance_with_shading',
+ 'direct_tilted_irradiance',
+ 'direct_tilted_irradiance_with_shading',
+ 'global_clear_sky_irradiance',
+ 'global_horizontal_irradiance',
+ 'global_horizontal_irradiance_with_shading',
+ 'global_tilted_irradiance',
+ 'global_tilted_irradiance_with_shading',
+ 'pv_production',
+ 'pv_production_with_shading',
+ 'snow_depth',
+ 'temperature',
+ ]
+ return columns
+
+
@pytest.mark.remote_data
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
def test_get_meteonorm_training(
@@ -170,6 +111,7 @@ def test_get_meteonorm_training(
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',
time_step='1h',
url=demo_url)
@@ -181,15 +123,13 @@ 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_meta, expected_meteonorm_index,
- expected_metenorm_data):
+def test_get_meteonorm_realtime(demo_api_key, demo_url, expected_columns_all):
data, meta = pvlib.iotools.get_meteonorm(
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=['ghi', 'global_horizontal_irradiance_with_shading'],
+ parameters=['all'],
api_key=demo_api_key,
endpoint='/observation/realtime',
time_step='1min',
@@ -204,10 +144,45 @@ def test_get_meteonorm_realtime(
assert meta['surface_tilt'] == 20
assert meta['surface_azimuth'] == 10
- assert all(data.columns == pd.Index([
- 'global_horizontal_irradiance',
- 'global_horizontal_irradiance_with_shading']))
- assert data.shape == (241, 2)
+ assert list(data.columns) == expected_columns_all
+ assert data.shape == (241, 19)
# can't test the specific index as it varies due to the
# use of pd.Timestamp.now
assert type(data.index) is pd.core.indexes.interval.IntervalIndex
+
+
+@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(
+ 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',
+ endpoint='forecast/basic',
+ url=demo_url)
+
+ assert data.shape == (6, 1)
+ assert data.columns == pd.Index(['ghi'])
+ assert data.index[1] - data.index[0] == pd.Timedelta(hours=1)
+ assert meta['frequency'] == '1_hour'
+
+
+@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(
+ 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',
+ # test that the time_step parameter is ignored
+ time_step='1h',
+ url=demo_url)
+
+ assert data.index[1] - data.index[0] == pd.Timedelta(minutes=15)
+ assert data.shape == (5, 1)
+ assert meta['frequency'] == '15_minutes'
From adda4cfe760d6f1b52a9953023cc4889b0b004d8 Mon Sep 17 00:00:00 2001
From: "Adam R. Jensen" <39184289+AdamRJensen@users.noreply.github.com>
Date: Sat, 2 Aug 2025 21:38:11 +0200
Subject: [PATCH 10/18] Full test coverage
---
pvlib/iotools/meteonorm.py | 18 +++--
tests/iotools/test_meteonorm.py | 122 +++++++++++++++++++++++++++++++-
2 files changed, 130 insertions(+), 10 deletions(-)
diff --git a/pvlib/iotools/meteonorm.py b/pvlib/iotools/meteonorm.py
index d58ce6e894..b61bbb0306 100644
--- a/pvlib/iotools/meteonorm.py
+++ b/pvlib/iotools/meteonorm.py
@@ -75,7 +75,7 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
forcasting data.
horizon : str or list, default : 'auto'
Specification of the horizon line. Can be either a 'flat', 'auto', or
- a list of 360 horizon elevation angles.
+ 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.
@@ -131,7 +131,8 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
# Allow specifying single parameters as string
if isinstance(parameters, str):
- parameter_list = list(VARIABLE_MAP.keys()) + list(VARIABLE_MAP.values())
+ parameter_list = \
+ list(VARIABLE_MAP.keys()) + list(VARIABLE_MAP.values())
if parameters in parameter_list:
parameters = [parameters]
@@ -198,7 +199,7 @@ def get_meteonorm_tmy(latitude, longitude, api_key,
Frequency of the time series.
horizon : str, optional
Specification of the horizon line. Can be either 'flat' or 'auto', or
- specified as a list of 360 horizon elevation angles.
+ specified as a list of 360 integer horizon elevation angles.
'auto'.
terrain : str, default : 'open'
Local terrain situation. Must be one of: ['open', 'depression',
@@ -265,7 +266,7 @@ def get_meteonorm_tmy(latitude, longitude, api_key,
'frequency': time_step,
'parameters': parameters,
'horizon': horizon,
- 'terrain': terrain,
+ 'situation': terrain,
'turbidity': turbidity,
'clear_sky_radiation_model': clear_sky_radiation_model,
'data_version': data_version,
@@ -276,7 +277,8 @@ def get_meteonorm_tmy(latitude, longitude, api_key,
# Allow specifying single parameters as string
if isinstance(parameters, str):
- parameter_list = list(VARIABLE_MAP.keys()) + list(VARIABLE_MAP.values())
+ parameter_list = \
+ list(VARIABLE_MAP.keys()) + list(VARIABLE_MAP.values())
if parameters in parameter_list:
parameters = [parameters]
@@ -287,12 +289,14 @@ def get_meteonorm_tmy(latitude, longitude, api_key,
parameters = [parameter_dict.get(p, p) for p in parameters]
params['parameters'] = ','.join(parameters)
- if isinstance(horizon, str):
+ if not isinstance(horizon, str):
params['horizon'] = ','.join(map(str, horizon))
- if isinstance(turbidity, str):
+ if not isinstance(turbidity, str):
params['turbidity'] = ','.join(map(str, turbidity))
+ params['frequency'] = TIME_STEP_MAP.get(time_step, time_step)
+
headers = {"Authorization": f"Bearer {api_key}"}
response = requests.get(
diff --git a/tests/iotools/test_meteonorm.py b/tests/iotools/test_meteonorm.py
index e809d2a41f..8a4a04541a 100644
--- a/tests/iotools/test_meteonorm.py
+++ b/tests/iotools/test_meteonorm.py
@@ -1,7 +1,9 @@
import pandas as pd
+import numpy as np
import pytest
import pvlib
from tests.conftest import RERUNS, RERUNS_DELAY
+from requests.exceptions import HTTPError
@pytest.fixture
@@ -29,11 +31,13 @@ def expected_meta():
{'aggregation_method': 'average',
'description': 'Global horizontal irradiance',
'name': 'global_horizontal_irradiance',
- 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
+ 'unit': {
+ 'description': 'Watt per square meter', 'name': 'W/m**2'}},
{'aggregation_method': 'average',
- 'description': 'Global horizontal irradiance with shading taken into account',
+ 'description': 'Global horizontal irradiance with shading taken into account', # noqa: E501
'name': 'global_horizontal_irradiance_with_shading',
- 'unit': {'description': 'Watt per square meter', 'name': 'W/m**2'}},
+ 'unit': {'description': 'Watt per square meter',
+ 'name': 'W/m**2'}},
],
'surface_azimuth': 180,
'surface_tilt': 0,
@@ -186,3 +190,115 @@ def test_get_meteonorm_forecast_precision(demo_api_key, demo_url):
assert data.index[1] - data.index[0] == pd.Timedelta(minutes=15)
assert data.shape == (5, 1)
assert meta['frequency'] == '15_minutes'
+
+
+@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(
+ 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',
+ endpoint='forecast/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):
+ with pytest.raises(
+ HTTPError, match="unknown parameter: not_a_real_parameter'"):
+ _ = pvlib.iotools.get_meteonorm(
+ 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='not_a_real_parameter',
+ endpoint='forecast/basic',
+ url=demo_url)
+
+
+@pytest.fixture
+def expected_meteonorm_tmy_meta():
+ meta = {
+ 'altitude': 290,
+ 'frequency': '1_hour',
+ 'parameters': [{
+ 'aggregation_method': 'average',
+ 'description': 'Diffuse horizontal irradiance',
+ 'name': 'diffuse_horizontal_irradiance',
+ 'unit': {'description': 'Watt per square meter',
+ 'name': 'W/m**2'},
+ }],
+ 'surface_azimuth': 90,
+ 'surface_tilt': 20,
+ 'time_zone': 1,
+ 'latitude': 50,
+ 'longitude': 10,
+ }
+ return meta
+
+
+@pytest.fixture
+def expected_meteonorm_tmy_index():
+ index = pd.date_range(
+ '2005-01-01', periods=8760, freq='1h', tz=3600)
+ index.freq = None
+ return index
+
+
+@pytest.fixture
+def expected_metenorm_tmy_data():
+ # The first 12 rows of data
+ columns = ['diffuse_horizontal_irradiance']
+ expected = [
+ [0.],
+ [0.],
+ [0.],
+ [0.],
+ [0.],
+ [0.],
+ [0.],
+ [0.],
+ [9.],
+ [8.4],
+ [86.6],
+ [110.5],
+ ]
+ index = pd.date_range(
+ '2005-01-01', periods=12, freq='1h', tz=3600)
+ index.freq = None
+ expected = pd.DataFrame(expected, index=index, columns=columns)
+ return expected
+
+
+# @pytest.mark.remote_data
+# @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
+def test_get_meteonorm_tmy(
+ demo_api_key, demo_url, expected_meteonorm_tmy_meta,
+ expected_metenorm_tmy_data, expected_meteonorm_tmy_index):
+ data, meta = pvlib.iotools.get_meteonorm_tmy(
+ latitude=50, longitude=10,
+ api_key=demo_api_key,
+ parameters='dhi',
+ surface_tilt=20,
+ surface_azimuth=90,
+ time_step='1h',
+ horizon=list(np.ones(360).astype(int)*2),
+ terrain='open',
+ albedo=0.5,
+ turbidity='auto',
+ random_seed=100,
+ clear_sky_radiation_model='solis',
+ data_version='v9.0', # fix version
+ future_scenario='ssp1_26',
+ future_year=2030,
+ interval_index=True,
+ map_variables=False,
+ url=demo_url)
+ assert meta == expected_meteonorm_tmy_meta
+ pd.testing.assert_frame_equal(data.iloc[:12], expected_metenorm_tmy_data)
+ pd.testing.assert_index_equal(data.index, expected_meteonorm_tmy_index)
From ea48630a27d48bfc9f2e85c45a6fe9ea50b0b5a9 Mon Sep 17 00:00:00 2001
From: "Adam R. Jensen" <39184289+AdamRJensen@users.noreply.github.com>
Date: Sat, 2 Aug 2025 21:40:02 +0200
Subject: [PATCH 11/18] Fix linter
---
tests/iotools/test_meteonorm.py | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/tests/iotools/test_meteonorm.py b/tests/iotools/test_meteonorm.py
index 8a4a04541a..36f95bb02f 100644
--- a/tests/iotools/test_meteonorm.py
+++ b/tests/iotools/test_meteonorm.py
@@ -38,7 +38,7 @@ def expected_meta():
'name': 'global_horizontal_irradiance_with_shading',
'unit': {'description': 'Watt per square meter',
'name': 'W/m**2'}},
- ],
+ ],
'surface_azimuth': 180,
'surface_tilt': 0,
'time_zone': 0,
@@ -141,7 +141,7 @@ def test_get_meteonorm_realtime(demo_api_key, demo_url, expected_columns_all):
map_variables=False,
interval_index=True,
url=demo_url,
- )
+ )
assert meta['frequency'] == '1_minute'
assert meta['lat'] == 21
assert meta['lon'] == 79
@@ -232,7 +232,7 @@ def expected_meteonorm_tmy_meta():
'name': 'diffuse_horizontal_irradiance',
'unit': {'description': 'Watt per square meter',
'name': 'W/m**2'},
- }],
+ }],
'surface_azimuth': 90,
'surface_tilt': 20,
'time_zone': 1,
@@ -275,8 +275,8 @@ def expected_metenorm_tmy_data():
return expected
-# @pytest.mark.remote_data
-# @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
+@pytest.mark.remote_data
+@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
def test_get_meteonorm_tmy(
demo_api_key, demo_url, expected_meteonorm_tmy_meta,
expected_metenorm_tmy_data, expected_meteonorm_tmy_index):
From ba0042375d9cf238710fd4b6ac2831c42d5ca2a7 Mon Sep 17 00:00:00 2001
From: "Adam R. Jensen" <39184289+AdamRJensen@users.noreply.github.com>
Date: Sat, 2 Aug 2025 22:04:23 +0200
Subject: [PATCH 12/18] Fix tests
---
pvlib/iotools/meteonorm.py | 2 +-
tests/iotools/test_meteonorm.py | 19 ++++++++++++-------
2 files changed, 13 insertions(+), 8 deletions(-)
diff --git a/pvlib/iotools/meteonorm.py b/pvlib/iotools/meteonorm.py
index b61bbb0306..e3cc7b4684 100644
--- a/pvlib/iotools/meteonorm.py
+++ b/pvlib/iotools/meteonorm.py
@@ -325,7 +325,7 @@ def _parse_meteonorm(response, interval_index, map_variables):
data.index = pd.IntervalIndex.from_arrays(
left=pd.to_datetime(response.json()['start_times']),
right=pd.to_datetime(response.json()['end_times']),
- closed='both',
+ closed='left',
)
else:
data.index = pd.to_datetime(response.json()['start_times'])
diff --git a/tests/iotools/test_meteonorm.py b/tests/iotools/test_meteonorm.py
index 36f95bb02f..42a10316b2 100644
--- a/tests/iotools/test_meteonorm.py
+++ b/tests/iotools/test_meteonorm.py
@@ -236,18 +236,20 @@ def expected_meteonorm_tmy_meta():
'surface_azimuth': 90,
'surface_tilt': 20,
'time_zone': 1,
- 'latitude': 50,
- 'longitude': 10,
+ 'lat': 50,
+ 'lon': 10,
}
return meta
@pytest.fixture
-def expected_meteonorm_tmy_index():
+def expected_meteonorm_tmy_interval_index():
index = pd.date_range(
'2005-01-01', periods=8760, freq='1h', tz=3600)
index.freq = None
- return index
+ interval_index = pd.IntervalIndex.from_arrays(
+ index, index + pd.Timedelta(hours=1), closed='left')
+ return interval_index
@pytest.fixture
@@ -271,7 +273,9 @@ def expected_metenorm_tmy_data():
index = pd.date_range(
'2005-01-01', periods=12, freq='1h', tz=3600)
index.freq = None
- expected = pd.DataFrame(expected, index=index, columns=columns)
+ interval_index = pd.IntervalIndex.from_arrays(
+ index, index + pd.Timedelta(hours=1), closed='left')
+ expected = pd.DataFrame(expected, index=interval_index, columns=columns)
return expected
@@ -279,7 +283,7 @@ def expected_metenorm_tmy_data():
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
def test_get_meteonorm_tmy(
demo_api_key, demo_url, expected_meteonorm_tmy_meta,
- expected_metenorm_tmy_data, expected_meteonorm_tmy_index):
+ expected_metenorm_tmy_data, expected_meteonorm_tmy_interval_index):
data, meta = pvlib.iotools.get_meteonorm_tmy(
latitude=50, longitude=10,
api_key=demo_api_key,
@@ -301,4 +305,5 @@ def test_get_meteonorm_tmy(
url=demo_url)
assert meta == expected_meteonorm_tmy_meta
pd.testing.assert_frame_equal(data.iloc[:12], expected_metenorm_tmy_data)
- pd.testing.assert_index_equal(data.index, expected_meteonorm_tmy_index)
+ pd.testing.assert_index_equal(
+ data.index, expected_meteonorm_tmy_interval_index)
From 83d00cbd2afe915bd2d09203df0d6a8c594e44ee Mon Sep 17 00:00:00 2001
From: "Adam R. Jensen" <39184289+AdamRJensen@users.noreply.github.com>
Date: Sat, 2 Aug 2025 22:19:37 +0200
Subject: [PATCH 13/18] Increase test coverage
---
pvlib/iotools/meteonorm.py | 9 +++++++++
tests/iotools/test_meteonorm.py | 16 +++++++++++++++-
2 files changed, 24 insertions(+), 1 deletion(-)
diff --git a/pvlib/iotools/meteonorm.py b/pvlib/iotools/meteonorm.py
index e3cc7b4684..d4716b29a3 100644
--- a/pvlib/iotools/meteonorm.py
+++ b/pvlib/iotools/meteonorm.py
@@ -100,6 +100,15 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
meta : dict
Metadata.
+ Examples
+ --------
+ >>> # Retrieve historical time series data
+ >>> df, meta = get_meteonorm( # 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
+
See Also
--------
pvlib.iotools.get_meteonorm_tmy
diff --git a/tests/iotools/test_meteonorm.py b/tests/iotools/test_meteonorm.py
index 42a10316b2..c16fafdf64 100644
--- a/tests/iotools/test_meteonorm.py
+++ b/tests/iotools/test_meteonorm.py
@@ -221,6 +221,20 @@ def test_get_meteonorm_HTTPError(demo_api_key, demo_url):
url=demo_url)
+@pytest.mark.remote_data
+@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
+def test_get_meteonorm_tmy_HTTPError(demo_api_key, demo_url):
+ with pytest.raises(
+ HTTPError, match='parameter "surface_azimuth"'):
+ _ = pvlib.iotools.get_meteonorm_tmy(
+ latitude=50, longitude=10,
+ api_key=demo_api_key,
+ parameters='dhi',
+ # Infeasible surface_titl
+ surface_azimuth=400,
+ url=demo_url)
+
+
@pytest.fixture
def expected_meteonorm_tmy_meta():
meta = {
@@ -294,7 +308,7 @@ def test_get_meteonorm_tmy(
horizon=list(np.ones(360).astype(int)*2),
terrain='open',
albedo=0.5,
- turbidity='auto',
+ turbidity=[5.2, 4, 3, 3.1, 3.0, 2.8, 3.14, 3.0, 3, 3, 4, 5],
random_seed=100,
clear_sky_radiation_model='solis',
data_version='v9.0', # fix version
From 239cff1b6234745e2a0dd78638ed290e527b1a54 Mon Sep 17 00:00:00 2001
From: "Adam R. Jensen" <39184289+AdamRJensen@users.noreply.github.com>
Date: Mon, 4 Aug 2025 17:41:01 +0200
Subject: [PATCH 14/18] Implement feedback from Meteonorm review
---
pvlib/iotools/meteonorm.py | 49 ++++++++++++++++-----------------
tests/iotools/test_meteonorm.py | 7 ++---
2 files changed, 27 insertions(+), 29 deletions(-)
diff --git a/pvlib/iotools/meteonorm.py b/pvlib/iotools/meteonorm.py
index d4716b29a3..22b7721d77 100644
--- a/pvlib/iotools/meteonorm.py
+++ b/pvlib/iotools/meteonorm.py
@@ -49,10 +49,10 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
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. Relative datetime strings are supported.
+ specified, UTC is assumed.
end : datetime like
Last timestamp of the requested period. If a timezone is not
- specified, UTC is assumed. Relative datetime strings are supported.
+ specified, UTC is assumed.
api_key : str
Meteonorm API key.
endpoint : str
@@ -61,7 +61,8 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
* ``'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 15-min resolution
+ * ``'forecast/precision'`` - forecast with 1-min, 15-min, or hourly
+ resolution
parameters : list or 'all', default : 'all'
List of parameters to request or `'all'` to get all parameters.
@@ -71,10 +72,9 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
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 parameter is ignored when requesting
- forcasting data.
+ Frequency of the time series.
horizon : str or list, default : 'auto'
- Specification of the horizon line. Can be either a 'flat', 'auto', or
+ 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.
@@ -103,7 +103,7 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
Examples
--------
>>> # Retrieve historical time series data
- >>> df, meta = get_meteonorm( # doctest: +SKIP
+ >>> df, meta = pvlib.iotools.get_meteonorm( # doctest: +SKIP
... latitude=50, longitude=10, # doctest: +SKIP
... start='2023-01-01', end='2025-01-01', # doctest: +SKIP
... api_key='redacted', # doctest: +SKIP
@@ -122,6 +122,7 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
.. [3] `Meteonorm API reference
`_
"""
+ # Relative date strings are not yet supported
start = pd.Timestamp(start)
end = pd.Timestamp(end)
start = start.tz_localize('UTC') if start.tzinfo is None else start
@@ -136,14 +137,12 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
'surface_tilt': surface_tilt,
'surface_azimuth': surface_azimuth,
'horizon': horizon,
+ 'response_format': 'json',
}
# Allow specifying single parameters as string
if isinstance(parameters, str):
- parameter_list = \
- list(VARIABLE_MAP.keys()) + list(VARIABLE_MAP.values())
- if parameters in parameter_list:
- parameters = [parameters]
+ parameters = [parameters]
# convert list to string with values separated by commas
if not isinstance(parameters, (str, type(None))):
@@ -155,7 +154,7 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
if not isinstance(horizon, str):
params['horizon'] = ','.join(map(str, horizon))
- if 'forecast' not in endpoint.lower():
+ if 'basic' not in endpoint:
params['frequency'] = TIME_STEP_MAP.get(time_step, time_step)
headers = {"Authorization": f"Bearer {api_key}"}
@@ -165,7 +164,8 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
if not response.ok:
# response.raise_for_status() does not give a useful error message
- raise requests.HTTPError(response.json())
+ raise requests.HTTPError("Meteonorm API returned an error: "
+ + response.json()['error']['message'])
data, meta = _parse_meteonorm(response, interval_index, map_variables)
@@ -177,8 +177,8 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
def get_meteonorm_tmy(latitude, longitude, api_key,
parameters='all', *, surface_tilt=0,
- surface_azimuth=180, time_step='15min', horizon='auto',
- terrain='open', albedo=0.2, turbidity='auto',
+ surface_azimuth=180, time_step='1h', horizon='auto',
+ terrain='open', albedo=None, turbidity='auto',
random_seed=None, clear_sky_radiation_model='esra',
data_version='latest', future_scenario=None,
future_year=None, interval_index=False,
@@ -214,8 +214,10 @@ def get_meteonorm_tmy(latitude, longitude, api_key,
Local terrain situation. Must be one of: ['open', 'depression',
'cold_air_lake', 'sea_lake', 'city', 'slope_south',
'slope_west_east'].
- albedo : float, default : 0.2
- Ground albedo. Albedo changes due to snow fall are modelled.
+ albedo : float, optional
+ Constant ground albedo. If no value is specified a baseline albedo of
+ 0.2 is used and albedo cahnges due to snow fall is modeled. If a value
+ is specified, then snow fall is not modeled.
turbidity : list or 'auto', optional
List of 12 monthly mean atmospheric Linke turbidity values. The default
is 'auto'.
@@ -272,7 +274,7 @@ def get_meteonorm_tmy(latitude, longitude, api_key,
'lon': longitude,
'surface_tilt': surface_tilt,
'surface_azimuth': surface_azimuth,
- 'frequency': time_step,
+ 'frequency': TIME_STEP_MAP.get(time_step, time_step),
'parameters': parameters,
'horizon': horizon,
'situation': terrain,
@@ -282,14 +284,12 @@ def get_meteonorm_tmy(latitude, longitude, api_key,
'random_seed': random_seed,
'future_scenario': future_scenario,
'future_year': future_year,
+ 'response_format': 'json',
}
# Allow specifying single parameters as string
if isinstance(parameters, str):
- parameter_list = \
- list(VARIABLE_MAP.keys()) + list(VARIABLE_MAP.values())
- if parameters in parameter_list:
- parameters = [parameters]
+ parameters = [parameters]
# convert list to string with values separated by commas
if not isinstance(parameters, (str, type(None))):
@@ -304,8 +304,6 @@ def get_meteonorm_tmy(latitude, longitude, api_key,
if not isinstance(turbidity, str):
params['turbidity'] = ','.join(map(str, turbidity))
- params['frequency'] = TIME_STEP_MAP.get(time_step, time_step)
-
headers = {"Authorization": f"Bearer {api_key}"}
response = requests.get(
@@ -313,7 +311,8 @@ def get_meteonorm_tmy(latitude, longitude, api_key,
if not response.ok:
# response.raise_for_status() does not give a useful error message
- raise requests.HTTPError(response.json())
+ raise requests.HTTPError("Meteonorm API returned an error: "
+ + response.json()['error']['message'])
data, meta = _parse_meteonorm(response, interval_index, map_variables)
diff --git a/tests/iotools/test_meteonorm.py b/tests/iotools/test_meteonorm.py
index c16fafdf64..b37c99a8fe 100644
--- a/tests/iotools/test_meteonorm.py
+++ b/tests/iotools/test_meteonorm.py
@@ -183,8 +183,7 @@ def test_get_meteonorm_forecast_precision(demo_api_key, demo_url):
api_key=demo_api_key,
parameters='ghi',
endpoint='forecast/precision',
- # test that the time_step parameter is ignored
- time_step='1h',
+ time_step='15min',
url=demo_url)
assert data.index[1] - data.index[0] == pd.Timedelta(minutes=15)
@@ -210,7 +209,7 @@ def test_get_meteonorm_custom_horizon(demo_api_key, demo_url):
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
def test_get_meteonorm_HTTPError(demo_api_key, demo_url):
with pytest.raises(
- HTTPError, match="unknown parameter: not_a_real_parameter'"):
+ HTTPError, match="unknown parameter: not_a_real_parameter"):
_ = pvlib.iotools.get_meteonorm(
latitude=50, longitude=10,
start=pd.Timestamp.now(tz='UTC'),
@@ -230,7 +229,7 @@ def test_get_meteonorm_tmy_HTTPError(demo_api_key, demo_url):
latitude=50, longitude=10,
api_key=demo_api_key,
parameters='dhi',
- # Infeasible surface_titl
+ # Infeasible surface_tilt
surface_azimuth=400,
url=demo_url)
From 8e3b1ece9318f27b8b9388a4848fd8346ff67731 Mon Sep 17 00:00:00 2001
From: "Adam R. Jensen" <39184289+AdamRJensen@users.noreply.github.com>
Date: Mon, 4 Aug 2025 22:03:27 +0200
Subject: [PATCH 15/18] Implement feedback from code review from kandersolar
---
pvlib/iotools/meteonorm.py | 27 ++++++++++++++-------------
1 file changed, 14 insertions(+), 13 deletions(-)
diff --git a/pvlib/iotools/meteonorm.py b/pvlib/iotools/meteonorm.py
index 22b7721d77..327edfa563 100644
--- a/pvlib/iotools/meteonorm.py
+++ b/pvlib/iotools/meteonorm.py
@@ -12,6 +12,9 @@
'direct_normal_irradiance': 'dni',
'direct_horizontal_irradiance': 'bhi',
'global_clear_sky_irradiance': 'ghi_clear',
+ 'diffuse_clear_sky_irradiance': 'dhi_clear',
+ 'direct_normal_clear_sky_irradiance': 'dni_clear',
+ 'direct_horizontal_clear_sky_irradiance': 'bhi_clear',
'diffuse_tilted_irradiance': 'poa_diffuse',
'direct_tilted_irradiance': 'poa_direct',
'global_tilted_irradiance': 'poa',
@@ -96,7 +99,7 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
-------
data : pd.DataFrame
Time series data. The index corresponds to the start (left) of the
- interval unless ``interval_index`` is set to False.
+ interval unless ``interval_index`` is set to True.
meta : dict
Metadata.
@@ -144,12 +147,11 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
if isinstance(parameters, str):
parameters = [parameters]
+ # 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]
# convert list to string with values separated by commas
- if not isinstance(parameters, (str, type(None))):
- # 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['parameters'] = ','.join(parameters)
+ params['parameters'] = ','.join(parameters)
if not isinstance(horizon, str):
params['horizon'] = ','.join(map(str, horizon))
@@ -216,7 +218,7 @@ def get_meteonorm_tmy(latitude, longitude, api_key,
'slope_west_east'].
albedo : float, optional
Constant ground albedo. If no value is specified a baseline albedo of
- 0.2 is used and albedo cahnges due to snow fall is modeled. If a value
+ 0.2 is used and albedo changes due to snow fall are modeled. If a value
is specified, then snow fall is not modeled.
turbidity : list or 'auto', optional
List of 12 monthly mean atmospheric Linke turbidity values. The default
@@ -252,7 +254,7 @@ def get_meteonorm_tmy(latitude, longitude, api_key,
-------
data : pd.DataFrame
Time series data. The index corresponds to the start (left) of the
- interval unless ``interval_index`` is set to False.
+ interval unless ``interval_index`` is set to True.
meta : dict
Metadata.
@@ -291,12 +293,11 @@ def get_meteonorm_tmy(latitude, longitude, api_key,
if isinstance(parameters, str):
parameters = [parameters]
+ # 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]
# convert list to string with values separated by commas
- if not isinstance(parameters, (str, type(None))):
- # 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['parameters'] = ','.join(parameters)
+ params['parameters'] = ','.join(parameters)
if not isinstance(horizon, str):
params['horizon'] = ','.join(map(str, horizon))
From b15a17002045b2fee8277d698325b223d84e6bd0 Mon Sep 17 00:00:00 2001
From: "Adam R. Jensen" <39184289+AdamRJensen@users.noreply.github.com>
Date: Tue, 5 Aug 2025 11:10:57 +0200
Subject: [PATCH 16/18] basic endpoint only support '1h', rename
terrain_situation
---
pvlib/iotools/meteonorm.py | 13 +++++++++----
tests/iotools/test_meteonorm.py | 19 ++++++++++++++++++-
2 files changed, 27 insertions(+), 5 deletions(-)
diff --git a/pvlib/iotools/meteonorm.py b/pvlib/iotools/meteonorm.py
index 327edfa563..bca82c734e 100644
--- a/pvlib/iotools/meteonorm.py
+++ b/pvlib/iotools/meteonorm.py
@@ -75,7 +75,8 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
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.
+ Frequency of the time series. The endpoint ``'forecast/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.
@@ -158,6 +159,10 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
if 'basic' not in endpoint:
params['frequency'] = TIME_STEP_MAP.get(time_step, time_step)
+ else:
+ if time_step not in ['1h', '1_hour']:
+ raise ValueError("The 'forecast/basic' api endpoint only "
+ "supports ``time_step='1h'``.")
headers = {"Authorization": f"Bearer {api_key}"}
@@ -180,7 +185,7 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
def get_meteonorm_tmy(latitude, longitude, api_key,
parameters='all', *, surface_tilt=0,
surface_azimuth=180, time_step='1h', horizon='auto',
- terrain='open', albedo=None, turbidity='auto',
+ terrain_situation='open', albedo=None, turbidity='auto',
random_seed=None, clear_sky_radiation_model='esra',
data_version='latest', future_scenario=None,
future_year=None, interval_index=False,
@@ -212,7 +217,7 @@ def get_meteonorm_tmy(latitude, longitude, api_key,
Specification of the horizon line. Can be either 'flat' or 'auto', or
specified as a list of 360 integer horizon elevation angles.
'auto'.
- terrain : str, default : 'open'
+ terrain_situation : str, default : 'open'
Local terrain situation. Must be one of: ['open', 'depression',
'cold_air_lake', 'sea_lake', 'city', 'slope_south',
'slope_west_east'].
@@ -279,7 +284,7 @@ def get_meteonorm_tmy(latitude, longitude, api_key,
'frequency': TIME_STEP_MAP.get(time_step, time_step),
'parameters': parameters,
'horizon': horizon,
- 'situation': terrain,
+ 'situation': terrain_situation,
'turbidity': turbidity,
'clear_sky_radiation_model': clear_sky_radiation_model,
'data_version': data_version,
diff --git a/tests/iotools/test_meteonorm.py b/tests/iotools/test_meteonorm.py
index b37c99a8fe..13153242a4 100644
--- a/tests/iotools/test_meteonorm.py
+++ b/tests/iotools/test_meteonorm.py
@@ -162,6 +162,7 @@ def test_get_meteonorm_forecast_basic(demo_api_key, demo_url):
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',
@@ -200,6 +201,7 @@ def test_get_meteonorm_custom_horizon(demo_api_key, demo_url):
end=pd.Timestamp.now(tz='UTC') + pd.Timedelta(hours=5),
api_key=demo_api_key,
parameters='ghi',
+ time_step='1h',
endpoint='forecast/basic',
horizon=list(np.ones(360).astype(int)*80),
url=demo_url)
@@ -214,12 +216,27 @@ def test_get_meteonorm_HTTPError(demo_api_key, demo_url):
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',
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(
+ 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',
+ url=demo_url)
+
+
@pytest.mark.remote_data
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
def test_get_meteonorm_tmy_HTTPError(demo_api_key, demo_url):
@@ -305,7 +322,7 @@ def test_get_meteonorm_tmy(
surface_azimuth=90,
time_step='1h',
horizon=list(np.ones(360).astype(int)*2),
- terrain='open',
+ terrain_situation='open',
albedo=0.5,
turbidity=[5.2, 4, 3, 3.1, 3.0, 2.8, 3.14, 3.0, 3, 3, 4, 5],
random_seed=100,
From 9129ad2b480f8eac1f4bb60641cbaa984a3b4800 Mon Sep 17 00:00:00 2001
From: "Adam R. Jensen" <39184289+AdamRJensen@users.noreply.github.com>
Date: Wed, 6 Aug 2025 00:47:42 +0200
Subject: [PATCH 17/18] Split get_meteonorm into forecast and observation
---
docs/sphinx/source/reference/iotools.rst | 3 +-
docs/sphinx/source/whatsnew/v0.13.1.rst | 3 +-
pvlib/iotools/__init__.py | 3 +-
pvlib/iotools/meteonorm.py | 154 +++++++++++++++++++----
tests/iotools/test_meteonorm.py | 30 ++---
5 files changed, 154 insertions(+), 39 deletions(-)
diff --git a/docs/sphinx/source/reference/iotools.rst b/docs/sphinx/source/reference/iotools.rst
index 75d18372a1..4fd53f1b40 100644
--- a/docs/sphinx/source/reference/iotools.rst
+++ b/docs/sphinx/source/reference/iotools.rst
@@ -98,7 +98,8 @@ Meteonorm
.. autosummary::
:toctree: generated/
- iotools.get_meteonorm
+ iotools.get_meteonorm_observation
+ iotools.get_meteonorm_forecast
iotools.get_meteonorm_tmy
diff --git a/docs/sphinx/source/whatsnew/v0.13.1.rst b/docs/sphinx/source/whatsnew/v0.13.1.rst
index 57a5baf73f..8ceac6af08 100644
--- a/docs/sphinx/source/whatsnew/v0.13.1.rst
+++ b/docs/sphinx/source/whatsnew/v0.13.1.rst
@@ -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`)
diff --git a/pvlib/iotools/__init__.py b/pvlib/iotools/__init__.py
index 5b510f2b8c..0907e83b9b 100644
--- a/pvlib/iotools/__init__.py
+++ b/pvlib/iotools/__init__.py
@@ -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
diff --git a/pvlib/iotools/meteonorm.py b/pvlib/iotools/meteonorm.py
index bca82c734e..c8819507a7 100644
--- a/pvlib/iotools/meteonorm.py
+++ b/pvlib/iotools/meteonorm.py
@@ -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
----------
@@ -58,14 +59,11 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
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.
@@ -75,8 +73,7 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
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.
@@ -87,8 +84,7 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
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
@@ -107,11 +103,11 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
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
--------
@@ -126,6 +122,120 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
.. [3] `Meteonorm API reference
`_
"""
+ endpoint_base = 'observation/'
+
+ data, meta = _get_meteonorm(
+ latitude, longitude, start, end, api_key,
+ 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
+ `_
+ .. [2] `Meteonorm API
+ `_
+ .. [3] `Meteonorm API reference
+ `_
+ """
+ endpoint_base = 'forecast/'
+
+ data, meta = _get_meteonorm(
+ latitude, longitude, start, end, api_key,
+ 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)
@@ -167,7 +277,8 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
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
@@ -265,7 +376,8 @@ def get_meteonorm_tmy(latitude, longitude, api_key,
See Also
--------
- pvlib.iotools.get_meteonorm
+ pvlib.iotools.get_meteonorm_observation,
+ pvlib.iotools.get_meteonorm_forecast
References
----------
diff --git a/tests/iotools/test_meteonorm.py b/tests/iotools/test_meteonorm.py
index 13153242a4..78b9f39f8c 100644
--- a/tests/iotools/test_meteonorm.py
+++ b/tests/iotools/test_meteonorm.py
@@ -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)
@@ -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,
@@ -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)
@@ -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)
@@ -195,31 +195,31 @@ 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)
@@ -227,13 +227,13 @@ 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)
From 3e9329f581d496448d8662487837de6f248dc7a4 Mon Sep 17 00:00:00 2001
From: "Adam R. Jensen" <39184289+AdamRJensen@users.noreply.github.com>
Date: Wed, 6 Aug 2025 00:53:16 +0200
Subject: [PATCH 18/18] Fix linter
---
pvlib/iotools/meteonorm.py | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/pvlib/iotools/meteonorm.py b/pvlib/iotools/meteonorm.py
index c8819507a7..de32d875f8 100644
--- a/pvlib/iotools/meteonorm.py
+++ b/pvlib/iotools/meteonorm.py
@@ -125,11 +125,11 @@ def get_meteonorm_observation(
endpoint_base = 'observation/'
data, meta = _get_meteonorm(
- latitude, longitude, start, end, api_key,
- endpoint_base, endpoint,
- parameters, surface_tilt, surface_azimuth,
- time_step, horizon, interval_index,
- map_variables, url)
+ latitude, longitude, start, end, api_key,
+ endpoint_base, endpoint,
+ parameters, surface_tilt, surface_azimuth,
+ time_step, horizon, interval_index,
+ map_variables, url)
return data, meta
@@ -221,11 +221,11 @@ def get_meteonorm_forecast(
endpoint_base = 'forecast/'
data, meta = _get_meteonorm(
- latitude, longitude, start, end, api_key,
- endpoint_base, endpoint,
- parameters, surface_tilt, surface_azimuth,
- time_step, horizon, interval_index,
- map_variables, url)
+ latitude, longitude, start, end, api_key,
+ endpoint_base, endpoint,
+ parameters, surface_tilt, surface_azimuth,
+ time_step, horizon, interval_index,
+ map_variables, url)
return data, meta