def mock_simple_manager_fail(): """Mock datapoint Manager with default values for testing in config_flow.""" with patch("datapoint.Manager") as mock_manager: instance = mock_manager.return_value instance.get_nearest_forecast_site.side_effect = APIException() instance.get_forecast_for_site.side_effect = APIException() instance.latitude = None instance.longitude = None instance.site = None instance.site_id = None instance.site_name = None instance.now = None yield mock_manager
def get_nearest_observation_site(self, latitude, longitude): """ This function returns the nearest Site to the specified coordinates that supports observations """ nearest = False distance = None sites = self.get_observation_sites() for site in sites: new_distance = \ self._distance_between_coords( float(site.longitude), float(site.latitude), float(longitude), float(latitude)) if ((distance == None) or (new_distance < distance)): distance = new_distance nearest = site # If the nearest site is more than 20km away, raise an error if distance > 20: raise APIException("There is no site within 30km.") return nearest
def get_nearest_forecast_site(self, latitude, longitude): """ This function returns the nearest Site object to the specified coordinates. """ nearest = False distance = None sites = self.get_forecast_sites() # Sometimes there is a TypeError exception here: sites is None # So, sometimes self.get_all_sites() has returned None. for site in sites: new_distance = \ self._distance_between_coords( float(site.longitude), float(site.latitude), float(longitude), float(latitude)) if ((distance == None) or (new_distance < distance)): distance = new_distance nearest = site # If the nearest site is more than 30km away, raise an error if distance > 30: raise APIException("There is no site within 30km.") return nearest
def __call_api(self, path, params=None, api_url=FORECAST_URL): """ Call the datapoint api using the requests module """ if not params: params = dict() payload = {'key': self.api_key} payload.update(params) url = "%s/%s" % (api_url, path) # Add a timeout to the request. # The value of 1 second is based on attempting 100 connections to # datapoint and taking ten times the mean connection time (rounded up). # Could expose to users in the functions which need to call the api. #req = requests.get(url, params=payload, timeout=1) # The wrapper function __retry_session returns a requests.Session # object. This has a .get() function like requests.get(), so the use # doesn't change here. sess = self.__retry_session() req = sess.get(url, params=payload, timeout=1) try: data = req.json() except ValueError: raise APIException( "DataPoint has not returned any data, this could be due to an incorrect API key" ) self.call_response = data if req.status_code != 200: msg = [data[m] for m in ("message", "error_message", "status") \ if m in data][0] raise Exception(msg) return data
def __call_api(self, path, params=None): """ Call the datapoint api using the requests module """ if not params: params = dict() payload = {'key': self.api_key} payload.update(params) url = "%s/%s" % (API_URL, path) req = requests.get(url, params=payload) try: data = req.json() except ValueError: raise APIException("DataPoint has not returned any data, this could be due to an incorrect API key") self.call_response = data if req.status_code != 200: msg = [data[m] for m in ("message", "error_message", "status") \ if m in data][0] raise Exception(msg) return data
def at_datetime(self, target): """ Return the timestep closest to the target datetime""" # Convert target to offset aware datetime if target.tzinfo is None: target = datetime.datetime.combine(target.date(), target.time(), self.days[0].date.tzinfo) num_timesteps = len(self.days[1].timesteps) # First check that the target is at most 1.5 hours before the first timestep if target < self.days[0].timesteps[0].date - datetime.timedelta( hours=1, minutes=30) and num_timesteps == 8: err_str = 'There is no forecast available for the requested time. ' + \ 'The requested time is more than 1.5 hours before the first available forecast' raise APIException(err_str) elif target < self.days[0].timesteps[0].date - datetime.timedelta( hours=6) and num_timesteps == 2: err_str = 'There is no forecast available for the requested time. ' + \ 'The requested time is more than 6 hours before the first available forecast' raise APIException(err_str) # Ensure that the target is less than 1 hour 30 minutes after the final # timestep. # Logic is correct # If there are 8 timesteps per day, then the target must be within 1.5 # hours of the last timestep if target > (self.days[-1].timesteps[-1].date + datetime.timedelta( hours=1, minutes=30)) and num_timesteps == 8: err_str = 'There is no forecast available for the requested time. The requested time is more than 1.5 hours after the first available forecast' raise APIException(err_str) # If there are 2 timesteps per day, then the target must be within 6 # hours of the last timestep if target > (self.days[-1].timesteps[-1].date + datetime.timedelta(hours=6)) and num_timesteps == 2: err_str = 'There is no forecast available for the requested time. The requested time is more than 6 hours after the first available forecast' raise APIException(err_str) # Loop over all timesteps # Calculate the first time difference prev_td = target - self.days[0].timesteps[0].date prev_ts = self.days[0].timesteps[0] for day in self.days: for timestep in day.timesteps: # Calculate the difference between the target time and the # timestep. td = target - timestep.date # Find the timestep which is further from the target than the # previous one. Return the previous timestep if abs(td.total_seconds()) > abs(prev_td.total_seconds()): # We are further from the target return prev_ts if abs(td.total_seconds()) < 5400 and num_timesteps == 8: # if we are past the final timestep, and it is a 3 hourly # forecast, check that we are within 90 minutes of it return timestep if abs(td.total_seconds()) < 21600 and num_timesteps == 2: # if we are past the final timestep, and it is a daily # forecast, check that we are within 6 hours of it return timestep prev_ts = timestep prev_td = td
def get_forecast_for_site(self, site_id, frequency="daily"): """ Get a forecast for the provided site A frequency of "daily" will return two timesteps: "Day" and "Night". A frequency of "3hourly" will return 8 timesteps: 0, 180, 360 ... 1260 (minutes since midnight UTC) """ data = self.__call_api(site_id, {"res": frequency}) params = data['SiteRep']['Wx']['Param'] forecast = Forecast() # If the 'Location' key is missing, there is no data for the site, # raise an error. if 'Location' not in data['SiteRep']['DV']: err_string = ('DataPoint has not returned any data for the' 'requested site.') raise APIException(err_string) # Check if the other keys we need are in the data returned from the # datapoint API. If they are not, the data elements in the python class # are left as None. It is currently the responsibility of the program # using them to cope with this. if 'dataDate' in data['SiteRep']['DV']: forecast.data_date = datetime.strptime( data['SiteRep']['DV']['dataDate'], DATA_DATE_FORMAT).replace(tzinfo=pytz.UTC) if 'continent' in data['SiteRep']['DV']['Location']: forecast.continent = data['SiteRep']['DV']['Location']['continent'] if 'country' in data['SiteRep']['DV']['Location']: forecast.country = data['SiteRep']['DV']['Location']['country'] if 'name' in data['SiteRep']['DV']['Location']: forecast.name = data['SiteRep']['DV']['Location']['name'] if 'lon' in data['SiteRep']['DV']['Location']: forecast.longitude = data['SiteRep']['DV']['Location']['lon'] if 'lat' in data['SiteRep']['DV']['Location']: forecast.latitude = data['SiteRep']['DV']['Location']['lat'] if 'i' in data['SiteRep']['DV']['Location']: forecast.id = data['SiteRep']['DV']['Location']['i'] if 'elevation' in data['SiteRep']['DV']['Location']: forecast.elevation = data['SiteRep']['DV']['Location']['elevation'] for day in data['SiteRep']['DV']['Location']['Period']: new_day = Day() new_day.date = datetime.strptime( day['value'], DATE_FORMAT).replace(tzinfo=pytz.UTC) for timestep in day['Rep']: new_timestep = Timestep() # According to the datapoint documentation # (https://www.metoffice.gov.uk/datapoint/product/uk-daily-site-specific-forecast), # the data provided are for noon (local time) and midnight # (local time). This implies that midnight is 00:00 and noon is # 12:00. if timestep['$'] == "Day": cur_elements = ELEMENTS['Day'] new_timestep.date = datetime.strptime(day['value'], DATE_FORMAT).replace(tzinfo=pytz.UTC) \ + timedelta(hours=12) elif timestep['$'] == "Night": cur_elements = ELEMENTS['Night'] new_timestep.date = datetime.strptime( day['value'], DATE_FORMAT).replace(tzinfo=pytz.UTC) else: cur_elements = ELEMENTS['Default'] new_timestep.date = datetime.strptime(day['value'], DATE_FORMAT).replace(tzinfo=pytz.UTC) \ + timedelta(minutes=int(timestep['$'])) if frequency == 'daily': new_timestep.name = timestep['$'] elif frequency == '3hourly': new_timestep.name = int(timestep['$']) new_timestep.weather = \ Element(cur_elements['W'], timestep[cur_elements['W']], self._get_wx_units(params, cur_elements['W'])) new_timestep.weather.text = self._weather_to_text( int(timestep[cur_elements['W']])) new_timestep.temperature = \ Element(cur_elements['T'], int(timestep[cur_elements['T']]), self._get_wx_units(params, cur_elements['T'])) new_timestep.feels_like_temperature = \ Element(cur_elements['F'], int(timestep[cur_elements['F']]), self._get_wx_units(params, cur_elements['F'])) new_timestep.wind_speed = \ Element(cur_elements['S'], int(timestep[cur_elements['S']]), self._get_wx_units(params, cur_elements['S'])) new_timestep.wind_direction = \ Element(cur_elements['D'], timestep[cur_elements['D']], self._get_wx_units(params, cur_elements['D'])) new_timestep.wind_gust = \ Element(cur_elements['G'], int(timestep[cur_elements['G']]), self._get_wx_units(params, cur_elements['G'])) new_timestep.visibility = \ Element(cur_elements['V'], timestep[cur_elements['V']], self._get_wx_units(params, cur_elements['V'])) new_timestep.precipitation = \ Element(cur_elements['Pp'], int(timestep[cur_elements['Pp']]), self._get_wx_units(params, cur_elements['Pp'])) new_timestep.humidity = \ Element(cur_elements['H'], int(timestep[cur_elements['H']]), self._get_wx_units(params, cur_elements['H'])) if 'U' in cur_elements and cur_elements['U'] in timestep: new_timestep.uv = \ Element(cur_elements['U'], timestep[cur_elements['U']], self._get_wx_units(params, cur_elements['U'])) new_day.timesteps.append(new_timestep) # The daily timesteps are not sorted by time. Sort them if frequency == 'daily': new_day.timesteps.sort(key=lambda r: r.date) forecast.days.append(new_day) return forecast