|
| 1 | +""" |
| 2 | +Get, read, and parse data from `PVGIS <https://ec.europa.eu/jrc/en/pvgis>`_. |
| 3 | +
|
| 4 | +For more information, see the following links: |
| 5 | +* `Interactive Tools <https://re.jrc.ec.europa.eu/pvg_tools/en/tools.html>`_ |
| 6 | +* `Data downloads <https://ec.europa.eu/jrc/en/PVGIS/downloads/data>`_ |
| 7 | +* `User manual docs <https://ec.europa.eu/jrc/en/PVGIS/docs/usermanual>`_ |
| 8 | +
|
| 9 | +More detailed information about the API for TMY and hourly radiation are here: |
| 10 | +* `TMY <https://ec.europa.eu/jrc/en/PVGIS/tools/tmy>`_ |
| 11 | +* `hourly radiation |
| 12 | + <https://ec.europa.eu/jrc/en/PVGIS/tools/hourly-radiation>`_ |
| 13 | +* `daily radiation <https://ec.europa.eu/jrc/en/PVGIS/tools/daily-radiation>`_ |
| 14 | +* `monthly radiation |
| 15 | + <https://ec.europa.eu/jrc/en/PVGIS/tools/monthly-radiation>`_ |
| 16 | +""" |
| 17 | +import io |
| 18 | +import requests |
| 19 | +import pandas as pd |
| 20 | +from pvlib.iotools import parse_epw |
| 21 | + |
| 22 | +URL = 'https://re.jrc.ec.europa.eu/api/' |
| 23 | + |
| 24 | + |
| 25 | +def get_pvgis_tmy(lat, lon, outputformat='json', usehorizon=True, |
| 26 | + userhorizon=None, startyear=None, endyear=None, url=URL, |
| 27 | + timeout=30): |
| 28 | + """ |
| 29 | + Get TMY data from PVGIS [1]_. For more information see the PVGIS TMY tool |
| 30 | + documentation [2]_. |
| 31 | +
|
| 32 | + Parameters |
| 33 | + ---------- |
| 34 | + lat : float |
| 35 | + Latitude in degrees north |
| 36 | + lon : float |
| 37 | + Longitude in dgrees east |
| 38 | + outputformat : str, default 'json' |
| 39 | + Must be in ``['csv', 'basic', 'epw', 'json']``. See PVGIS TMY tool |
| 40 | + documentation [2]_ for more info. |
| 41 | + usehorizon : bool, default True |
| 42 | + include effects of horizon |
| 43 | + userhorizon : list of float, default None |
| 44 | + optional user specified elevation of horizon in degrees, at equally |
| 45 | + spaced azimuth clockwise from north, only valid if `usehorizon` is |
| 46 | + true, if `usehorizon` is true but `userhorizon` is `None` then PVGIS |
| 47 | + will calculate the horizon [3]_ |
| 48 | + startyear : int, default None |
| 49 | + first year to calculate TMY |
| 50 | + endyear : int, default None |
| 51 | + last year to calculate TMY, must be at least 10 years from first year |
| 52 | + url : str, default :const:`pvlib.iotools.pvgis.URL` |
| 53 | + base url of PVGIS API, append ``tmy`` to get TMY endpoint |
| 54 | + timeout : int, default 30 |
| 55 | + time in seconds to wait for server response before timeout |
| 56 | +
|
| 57 | + Returns |
| 58 | + ------- |
| 59 | + data : pandas.DataFrame |
| 60 | + the weather data |
| 61 | + months_selected : list |
| 62 | + TMY year for each month, ``None`` for basic and EPW |
| 63 | + inputs : dict |
| 64 | + the inputs, ``None`` for basic and EPW |
| 65 | + meta : list or dict |
| 66 | + meta data, ``None`` for basic |
| 67 | +
|
| 68 | + Raises |
| 69 | + ------ |
| 70 | + requests.HTTPError |
| 71 | + if the request response status is ``HTTP/1.1 400 BAD REQUEST``, then |
| 72 | + the error message in the response will be raised as an exception, |
| 73 | + otherwise raise whatever ``HTTP/1.1`` error occurred |
| 74 | +
|
| 75 | + References |
| 76 | + ---------- |
| 77 | +
|
| 78 | + .. [1] `PVGIS <https://ec.europa.eu/jrc/en/pvgis>`_ |
| 79 | + .. [2] `PVGIS TMY tool <https://ec.europa.eu/jrc/en/PVGIS/tools/tmy>`_ |
| 80 | + .. [3] `PVGIS horizon profile tool |
| 81 | + <https://ec.europa.eu/jrc/en/PVGIS/tools/horizon>`_ |
| 82 | + """ |
| 83 | + # use requests to format the query string by passing params dictionary |
| 84 | + params = {'lat': lat, 'lon': lon, 'outputformat': outputformat} |
| 85 | + # pvgis only likes 0 for False, and 1 for True, not strings, also the |
| 86 | + # default for usehorizon is already 1 (ie: True), so only set if False |
| 87 | + if not usehorizon: |
| 88 | + params['usehorizon'] = 0 |
| 89 | + if userhorizon is not None: |
| 90 | + params['userhorizon'] = ','.join(str(x) for x in userhorizon) |
| 91 | + if startyear is not None: |
| 92 | + params['startyear'] = startyear |
| 93 | + if endyear is not None: |
| 94 | + params['endyear'] = endyear |
| 95 | + res = requests.get(url + 'tmy', params=params, timeout=timeout) |
| 96 | + # PVGIS returns really well formatted error messages in JSON for HTTP/1.1 |
| 97 | + # 400 BAD REQUEST so try to return that if possible, otherwise raise the |
| 98 | + # HTTP/1.1 error caught by requests |
| 99 | + if not res.ok: |
| 100 | + try: |
| 101 | + err_msg = res.json() |
| 102 | + except Exception: |
| 103 | + res.raise_for_status() |
| 104 | + else: |
| 105 | + raise requests.HTTPError(err_msg['message']) |
| 106 | + # initialize data to None in case API fails to respond to bad outputformat |
| 107 | + data = None, None, None, None |
| 108 | + if outputformat == 'json': |
| 109 | + src = res.json() |
| 110 | + return _parse_pvgis_tmy_json(src) |
| 111 | + elif outputformat == 'csv': |
| 112 | + with io.BytesIO(res.content) as src: |
| 113 | + data = _parse_pvgis_tmy_csv(src) |
| 114 | + elif outputformat == 'basic': |
| 115 | + with io.BytesIO(res.content) as src: |
| 116 | + data = _parse_pvgis_tmy_basic(src) |
| 117 | + elif outputformat == 'epw': |
| 118 | + with io.StringIO(res.content.decode('utf-8')) as src: |
| 119 | + data, meta = parse_epw(src) |
| 120 | + data = (data, None, None, meta) |
| 121 | + else: |
| 122 | + # this line is never reached because if outputformat is not valid then |
| 123 | + # the response is HTTP/1.1 400 BAD REQUEST which is handled earlier |
| 124 | + pass |
| 125 | + return data |
| 126 | + |
| 127 | + |
| 128 | +def _parse_pvgis_tmy_json(src): |
| 129 | + inputs = src['inputs'] |
| 130 | + meta = src['meta'] |
| 131 | + months_selected = src['outputs']['months_selected'] |
| 132 | + data = pd.DataFrame(src['outputs']['tmy_hourly']) |
| 133 | + data.index = pd.to_datetime( |
| 134 | + data['time(UTC)'], format='%Y%m%d:%H%M', utc=True) |
| 135 | + data = data.drop('time(UTC)', axis=1) |
| 136 | + return data, months_selected, inputs, meta |
| 137 | + |
| 138 | + |
| 139 | +def _parse_pvgis_tmy_csv(src): |
| 140 | + # the first 3 rows are latitude, longitude, elevation |
| 141 | + inputs = {} |
| 142 | + # 'Latitude (decimal degrees): 45.000\r\n' |
| 143 | + inputs['latitude'] = float(src.readline().split(b':')[1]) |
| 144 | + # 'Longitude (decimal degrees): 8.000\r\n' |
| 145 | + inputs['longitude'] = float(src.readline().split(b':')[1]) |
| 146 | + # Elevation (m): 1389.0\r\n |
| 147 | + inputs['elevation'] = float(src.readline().split(b':')[1]) |
| 148 | + # then there's a 13 row comma separated table with two columns: month, year |
| 149 | + # which contains the year used for that month in the |
| 150 | + src.readline() # get "month,year\r\n" |
| 151 | + months_selected = [] |
| 152 | + for month in range(12): |
| 153 | + months_selected.append( |
| 154 | + {'month': month+1, 'year': int(src.readline().split(b',')[1])}) |
| 155 | + # then there's the TMY (typical meteorological year) data |
| 156 | + # first there's a header row: |
| 157 | + # time(UTC),T2m,RH,G(h),Gb(n),Gd(h),IR(h),WS10m,WD10m,SP |
| 158 | + headers = [h.decode('utf-8').strip() for h in src.readline().split(b',')] |
| 159 | + data = pd.DataFrame( |
| 160 | + [src.readline().split(b',') for _ in range(8760)], columns=headers) |
| 161 | + dtidx = data['time(UTC)'].apply(lambda dt: dt.decode('utf-8')) |
| 162 | + dtidx = pd.to_datetime(dtidx, format='%Y%m%d:%H%M', utc=True) |
| 163 | + data = data.drop('time(UTC)', axis=1) |
| 164 | + data = pd.DataFrame(data, dtype=float) |
| 165 | + data.index = dtidx |
| 166 | + # finally there's some meta data |
| 167 | + meta = [line.decode('utf-8').strip() for line in src.readlines()] |
| 168 | + return data, months_selected, inputs, meta |
| 169 | + |
| 170 | + |
| 171 | +def _parse_pvgis_tmy_basic(src): |
| 172 | + data = pd.read_csv(src) |
| 173 | + data.index = pd.to_datetime( |
| 174 | + data['time(UTC)'], format='%Y%m%d:%H%M', utc=True) |
| 175 | + data = data.drop('time(UTC)', axis=1) |
| 176 | + return data, None, None, None |
0 commit comments