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
Example #2
0
    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
Example #3
0
    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
Example #4
0
    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
Example #5
0
 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
Example #6
0
    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
Example #7
0
    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