Skip to content

Commit 95c2602

Browse files
committed
Add get_meteonorm_tmy
1 parent 7ca85bb commit 95c2602

4 files changed

Lines changed: 205 additions & 19 deletions

File tree

docs/sphinx/source/reference/iotools.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,16 @@ Commercial datasets
8181
Accessing these APIs typically requires payment.
8282
Datasets provide near-global coverage.
8383

84+
Meteonorm
85+
*********
86+
87+
.. autosummary::
88+
:toctree: generated/
89+
90+
iotools.get_meteonorm
91+
iotools.get_meteonorm_tmy
92+
93+
8494
SolarAnywhere
8595
*************
8696

docs/sphinx/source/whatsnew/v0.13.1.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ Bug fixes
1919

2020
Enhancements
2121
~~~~~~~~~~~~
22-
22+
* Add iotools functions to retrieve irradiance and weather data from Meteonorm:
23+
:py:func:`~pvlib.iotools.get_meteonorm` and :py:func:`~pvlib.iotools.get_meteonorm_tmy`.
24+
(:pull:`2499`)
2325

2426
Documentation
2527
~~~~~~~~~~~~~

pvlib/iotools/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,5 @@
3939
from pvlib.iotools.solcast import get_solcast_historic # noqa: F401
4040
from pvlib.iotools.solcast import get_solcast_tmy # noqa: F401
4141
from pvlib.iotools.solargis import get_solargis # noqa: F401
42+
from pvlib.iotools.meteonorm import get_meteonorm # noqa: F401
43+
from pvlib.iotools.meteonorm import get_meteonorm_tmy # noqa: F401

pvlib/iotools/meteonorm.py

Lines changed: 190 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030

3131
def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
32-
parameters="all", *, surface_tilt=0, surface_azimuth=180,
32+
parameters='all', *, surface_tilt=0, surface_azimuth=180,
3333
time_step='15min', horizon='auto', interval_index=False,
3434
map_variables=True, url=URL):
3535
"""
@@ -38,9 +38,7 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
3838
The Meteonorm data options are described in [1]_ and the API is described
3939
in [2]_. A detailed list of API options can be found in [3]_.
4040
41-
This function supports the end points 'realtime' for data for the past 7
42-
days, 'training' for historical data with a delay of 7 days. The function
43-
does not support TMY climate data.
41+
This function supports both historical and forecast data, but not TMY.
4442
4543
Parameters
4644
----------
@@ -57,28 +55,27 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
5755
api_key: str
5856
Meteonorm API key.
5957
endpoint : str
60-
API end point, see [3]_. Must be one of:
58+
API endpoint, see [3]_. Must be one of:
6159
62-
* '/observation/training'
63-
* '/observation/realtime'
64-
* '/forecast/basic'
65-
* '/forecast/precision'
60+
* '/observation/training' - historical data with a 7-day delay
61+
* '/observation/realtime' - near-real time (past 7-days)
62+
* '/forecast/basic' - forcasts with hourly resolution
63+
* '/forecast/precision' - forecsat with 15-min resolution
6664
6765
parameters : list, optional
68-
List of parameters to request or "all" to get all parameters. The
66+
List of parameters to request or 'all' to get all parameters. The
6967
default is "all".
7068
surface_tilt: float, default: 0
7169
Tilt angle from horizontal plane.
7270
surface_azimuth: float, default: 180
7371
Orientation (azimuth angle) of the (fixed) plane. Clockwise from north
7472
(north=0, east=90, south=180, west=270).
7573
time_step : {'1min', '15min', '1h'}, optional
76-
ime step of the time series. The default is '15min'. Ignored if
77-
requesting forecast data.
74+
Frequency of the time series. The default is '15min'. The parameter is
75+
ignored if forcasting data is requested.
7876
horizon : optional
79-
Specification of the hoirzon line. Can be either 'flat' or 'auto', or
80-
specified as a list of 360 horizon elevation angles. The default is
81-
'auto'.
77+
Specification of the horizon line. Can be either a flat, 'auto', or
78+
a list of 360 horizon elevation angles. The default is 'auto'.
8279
interval_index: bool, optional
8380
Whether the index of the returned data object is of the type
8481
pd.DatetimeIndex or pd.IntervalIndex. This is an experimental feature
@@ -87,9 +84,10 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
8784
When true, renames columns of the Dataframe to pvlib variable names
8885
where applicable. The default is True. See variable
8986
:const:`VARIABLE_MAP`.
90-
url: str, default: :const:`pvlib.iotools.meteonorm.URL`
87+
url: str, optional
9188
Base url of the Meteonorm API. The ``endpoint`` parameter is
92-
appended to the url.
89+
appended to the url. The default is
90+
:const:`pvlib.iotools.meteonorm.URL`.
9391
9492
Raises
9593
------
@@ -145,7 +143,181 @@ def get_meteonorm(latitude, longitude, start, end, api_key, endpoint,
145143

146144
headers = {"Authorization": f"Bearer {api_key}"}
147145

148-
response = requests.get(urljoin(url, endpoint), headers=headers, params=params)
146+
response = requests.get(
147+
urljoin(url, endpoint), headers=headers, params=params)
148+
149+
if not response.ok:
150+
# response.raise_for_status() does not give a useful error message
151+
raise requests.HTTPError(response.json())
152+
153+
data_json = response.json()['values']
154+
# identify empty columns
155+
empty_columns = [k for k, v in data_json.items() if v is None]
156+
# remove empty columns
157+
_ = [data_json.pop(k) for k in empty_columns]
158+
159+
data = pd.DataFrame(data_json)
160+
161+
# xxx: experimental feature - see parameter description
162+
if interval_index:
163+
data.index = pd.IntervalIndex.from_arrays(
164+
left=pd.to_datetime(response.json()['start_times']),
165+
right=pd.to_datetime(response.json()['end_times']),
166+
closed='both',
167+
)
168+
else:
169+
data.index = pd.to_datetime(response.json()['start_times'])
170+
171+
meta = response.json()['meta']
172+
173+
if map_variables:
174+
data = data.rename(columns=VARIABLE_MAP)
175+
meta['latitude'] = meta.pop('lat')
176+
meta['longitude'] = meta.pop('lon')
177+
178+
return data, meta
179+
180+
181+
def get_meteonorm_tmy(latitude, longitude, api_key,
182+
parameters='all', *, surface_tilt=0,
183+
surface_azimuth=180, time_step='15min', horizon='auto',
184+
terrain='open', albedo=0.2, turbidity='auto',
185+
random_seed=None, clear_sky_radiation_model='esra',
186+
data_version='latest', future_scenario=None,
187+
future_year=None, interval_index=False,
188+
map_variables=True, url=URL):
189+
"""
190+
Retrieve irradiance and weather data from Meteonorm.
191+
192+
The Meteonorm data options are described in [1]_ and the API is described
193+
in [2]_. A detailed list of API options can be found in [3]_.
194+
195+
This function supports the endpoints 'realtime' for data for the past 7
196+
days, 'training' for historical data with a delay of 7 days. The function
197+
does not support TMY climate data.
198+
199+
Parameters
200+
----------
201+
latitude: float
202+
In decimal degrees, north is positive (ISO 19115).
203+
longitude: float
204+
In decimal degrees, east is positive (ISO 19115).
205+
api_key: str
206+
Meteonorm API key.
207+
parameters: list, optional
208+
List of parameters to request or 'all' to get all parameters. The
209+
default is 'all'.
210+
surface_tilt: float, default: 0
211+
Tilt angle from horizontal plane.
212+
surface_azimuth : float, default: 180
213+
Orientation (azimuth angle) of the (fixed) plane. Clockwise from north
214+
(north=0, east=90, south=180, west=270).
215+
time_step: {'1min', '1h'}, optional
216+
Frequency of the time series. The default is '1h'.
217+
horizon: optional
218+
Specification of the hoirzon line. Can be either 'flat' or 'auto', or
219+
specified as a list of 360 horizon elevation angles. The default is
220+
'auto'.
221+
terrain: string, optional
222+
Local terrain situation. Must be one of: ['open', 'depression',
223+
'cold_air_lake', 'sea_lake', 'city', 'slope_south',
224+
'slope_west_east']. The default is 'open'.
225+
albedo: float, optional
226+
Ground albedo. Albedo changes due to snow fall are modelled. The
227+
default is 0.2.
228+
turbidity: list or 'auto', optional
229+
List of 12 monthly mean atmospheric Linke turbidity values. The default
230+
is 'auto'.
231+
random_seed: int, optional
232+
Random seed to be used for stochastic processes. Two identical requests
233+
with the same random seed will yield identical results.
234+
clear_sky_radiation_model : {'esra', 'solis'}
235+
Which clearsky model to use. The default is 'esra'.
236+
data_version : string, optional
237+
Version of Meteonorm climatological data to be used. The default is
238+
'latest'.
239+
future_scenario: string, optional
240+
Future climate scenario.
241+
future_year : integer, optional
242+
Central year for a 20-year reference period in the future.
243+
interval_index: bool, optional
244+
Whether the index of the returned data object is of the type
245+
pd.DatetimeIndex or pd.IntervalIndex. This is an experimental feature
246+
which may be removed without warning. The default is False.
247+
map_variables: bool, default: True
248+
When true, renames columns of the Dataframe to pvlib variable names
249+
where applicable. The default is True. See variable
250+
:const:`VARIABLE_MAP`.
251+
url: str, optional.
252+
Base url of the Meteonorm API. 'climate/tmy'` is
253+
appended to the url. The default is:
254+
:const:`pvlib.iotools.meteonorm.URL`.
255+
256+
Raises
257+
------
258+
requests.HTTPError
259+
Raises an error when an incorrect request is made.
260+
261+
Returns
262+
-------
263+
data : pd.DataFrame
264+
Time series data. The index corresponds to the start (left) of the
265+
interval.
266+
meta : dict
267+
Metadata.
268+
269+
See Also
270+
--------
271+
pvlib.iotools.get_meteonorm
272+
273+
References
274+
----------
275+
.. [1] `Meteonorm
276+
<https://meteonorm.com/>`_
277+
.. [2] `Meteonorm API
278+
<https://docs.meteonorm.com/docs/getting-started>`_
279+
.. [3] `Meteonorm API reference
280+
<https://docs.meteonorm.com/api>`_
281+
"""
282+
params = {
283+
'lat': latitude,
284+
'lon': longitude,
285+
'surface_tilt': surface_tilt,
286+
'surface_azimuth': surface_azimuth,
287+
'frequency': time_step,
288+
'parameters': parameters,
289+
'horizon': horizon,
290+
'terrain': terrain,
291+
'turbidity': turbidity,
292+
'clear_sky_radiation_model': clear_sky_radiation_model,
293+
'data_version': data_version,
294+
}
295+
296+
if turbidity != 'auto':
297+
params['turbidity'] = ','.join(turbidity)
298+
299+
if random_seed is not None:
300+
params['random_seed'] = random_seed
301+
302+
if future_scenario is not None:
303+
params['future_scenario'] = future_scenario
304+
305+
if future_year is not None:
306+
params['future_year'] = future_year
307+
308+
# convert list to string with values separated by commas
309+
if not isinstance(params['parameters'], (str, type(None))):
310+
# allow the use of pvlib parameter names
311+
parameter_dict = {v: k for k, v in VARIABLE_MAP.items()}
312+
parameters = [parameter_dict.get(p, p) for p in parameters]
313+
params['parameters'] = ','.join(parameters)
314+
315+
headers = {"Authorization": f"Bearer {api_key}"}
316+
317+
endpoint = 'climate/tmy'
318+
319+
response = requests.get(
320+
urljoin(url, endpoint), headers=headers, params=params)
149321

150322
if not response.ok:
151323
# response.raise_for_status() does not give a useful error message

0 commit comments

Comments
 (0)