From 691f2bf0c64452b11a84094de7feefb6fe2c5b11 Mon Sep 17 00:00:00 2001 From: powlo Date: Thu, 13 Jul 2023 19:34:08 +0100 Subject: [PATCH] [weather.metoffice] v4.0.0 --- weather.metoffice/addon.xml | 2 +- weather.metoffice/changelog.txt | 7 +- weather.metoffice/resources/settings.xml | 12 +- weather.metoffice/src/default.py | 74 +- weather.metoffice/src/metoffice/astronomy.py | 95 ++- weather.metoffice/src/metoffice/constants.py | 330 +++++---- weather.metoffice/src/metoffice/properties.py | 653 ++++++++---------- weather.metoffice/src/metoffice/urlcache.py | 53 +- weather.metoffice/src/metoffice/utilities.py | 96 ++- weather.metoffice/src/setlocation.py | 137 ++-- 10 files changed, 770 insertions(+), 689 deletions(-) diff --git a/weather.metoffice/addon.xml b/weather.metoffice/addon.xml index 3a1c8d4a23..7d899e8eb5 100644 --- a/weather.metoffice/addon.xml +++ b/weather.metoffice/addon.xml @@ -1,5 +1,5 @@ - + diff --git a/weather.metoffice/changelog.txt b/weather.metoffice/changelog.txt index a8c42d4e61..56fc2caa79 100644 --- a/weather.metoffice/changelog.txt +++ b/weather.metoffice/changelog.txt @@ -20,4 +20,9 @@ v2.0.1 - added user agent to requests v3.0.0 -- updated for Matrix \ No newline at end of file +- updated for Matrix + +v4.0.0 +- Cleaned up memory leaks +- Removed unused imaging code. +- Removed default API key and nudged user to get their own. \ No newline at end of file diff --git a/weather.metoffice/resources/settings.xml b/weather.metoffice/resources/settings.xml index a37d915160..57fe05e534 100644 --- a/weather.metoffice/resources/settings.xml +++ b/weather.metoffice/resources/settings.xml @@ -1,13 +1,16 @@ - + - + - + @@ -15,7 +18,8 @@ - + \ No newline at end of file diff --git a/weather.metoffice/src/default.py b/weather.metoffice/src/default.py index 0edab66e3c..f5abcd2e84 100644 --- a/weather.metoffice/src/default.py +++ b/weather.metoffice/src/default.py @@ -14,52 +14,76 @@ # * along with XBMC; see the file COPYING. If not, write to # * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. # * http://www.gnu.org/copyleft/gpl.html -from metoffice.utilities import gettext as _ -from metoffice.constants import WINDOW, ADDON, API_KEY, ADDON_DATA_PATH, ADDON_BANNER_PATH -from metoffice import urlcache, properties, utilities import socket import sys +from urllib.error import HTTPError + +import xbmc import setlocation +from metoffice import properties, urlcache, utilities +from metoffice.constants import ( + ADDON_BANNER_PATH, + ADDON_DATA_PATH, + API_KEY, + addon, + window, +) +from metoffice.utilities import gettext as _ socket.setdefaulttimeout(20) -@utilities.failgracefully def main(): - if ADDON.getSetting('EraseCache') == 'true': + if addon().getSetting("EraseCache") == "true": try: urlcache.URLCache(ADDON_DATA_PATH).erase() finally: - ADDON.setSetting('EraseCache', 'false') + addon().setSetting("EraseCache", "false") if not API_KEY: - raise Exception(_("No API Key."), _("Enter your Met Office API Key under settings.")) + raise Exception( + _("No API Key."), _("Enter your Met Office API Key under settings.") + ) - if sys.argv[1] in ['ObservationLocation', 'ForecastLocation', 'RegionalLocation']: + if sys.argv[1] in ["ObservationLocation", "ForecastLocation", "RegionalLocation"]: setlocation.main(sys.argv[1]) - properties.observation() - properties.daily() - properties.threehourly() - properties.sunrisesunset() + try: + properties.observation() + properties.daily() + properties.threehourly() + properties.sunrisesunset() + except HTTPError: + utilities.log( + ( + "Error fetching data.\n" + "Ensure the API key in addon configuration is correct and try again.\n" + "You can get an API key by creating an account at\n" + "https://register.metoffice.gov.uk/WaveRegistrationClient/public/register.do?service=datapoint" + ), + xbmc.LOGERROR, + ) + raise - WINDOW.setProperty('WeatherProvider', ADDON.getAddonInfo('name')) - WINDOW.setProperty('WeatherProviderLogo', ADDON_BANNER_PATH) - WINDOW.setProperty('ObservationLocation', ADDON.getSetting('ObservationLocation')) - WINDOW.setProperty('Current.Location', ADDON.getSetting('ForecastLocation')) - WINDOW.setProperty('ForecastLocation', ADDON.getSetting('ForecastLocation')) - WINDOW.setProperty('RegionalLocation', ADDON.getSetting('RegionalLocation')) - WINDOW.setProperty('Location1', ADDON.getSetting('ForecastLocation')) - WINDOW.setProperty('Locations', '1') + window().setProperty("WeatherProvider", addon().getAddonInfo("name")) + window().setProperty("WeatherProviderLogo", ADDON_BANNER_PATH) + window().setProperty( + "ObservationLocation", addon().getSetting("ObservationLocation") + ) + window().setProperty("Current.Location", addon().getSetting("ForecastLocation")) + window().setProperty("ForecastLocation", addon().getSetting("ForecastLocation")) + window().setProperty("RegionalLocation", addon().getSetting("RegionalLocation")) + window().setProperty("Location1", addon().getSetting("ForecastLocation")) + window().setProperty("Locations", "1") # Explicitly set unused flags to false, so there are no unusual side # effects/residual data when moving from another weather provider. - WINDOW.setProperty('36Hour.IsFetched', '') - WINDOW.setProperty('Weekend.IsFetched', '') - WINDOW.setProperty('Map.IsFetched', '') - WINDOW.setProperty('Weather.CurrentView', '') + window().setProperty("36Hour.IsFetched", "") + window().setProperty("Weekend.IsFetched", "") + window().setProperty("Map.IsFetched", "") + window().setProperty("Weather.CurrentView", "") -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/weather.metoffice/src/metoffice/astronomy.py b/weather.metoffice/src/metoffice/astronomy.py index 1b5c6b0c53..ab8bca0e1c 100644 --- a/weather.metoffice/src/metoffice/astronomy.py +++ b/weather.metoffice/src/metoffice/astronomy.py @@ -2,9 +2,11 @@ Sunrise and Sunset calculations courtesy of Michel Anders http://michelanders.blogspot.co.uk/2010/12/calulating-sunrise-and-sunset-in-python.html """ -from math import cos, sin, acos, asin, tan -from math import degrees as deg, radians as rad from datetime import datetime, time +from math import acos, asin, cos +from math import degrees as deg +from math import radians as rad +from math import sin, tan from .constants import TZ @@ -63,11 +65,11 @@ def __timefromdecimalday(day): day is a decimal day between 0.0 and 1.0, e.g. noon = 0.5 """ - hours = 24.0*day + hours = 24.0 * day h = int(hours) - minutes = (hours-h)*60 + minutes = (hours - h) * 60 m = int(minutes) - seconds = (minutes-m)*60 + seconds = (minutes - m) * 60 s = int(seconds) return time(hour=h, minute=m, second=s) @@ -81,14 +83,14 @@ def __preptime(self, when): # OpenOffice spreadsheets with days numbered from # 1/1/1900. The difference are those numbers taken for # 18/12/2010 - self.day = when.toordinal()-(734124-40529) + self.day = when.toordinal() - (734124 - 40529) t = when.time() - self.time = (t.hour + t.minute/60.0 + t.second/3600.0)/24.0 + self.time = (t.hour + t.minute / 60.0 + t.second / 3600.0) / 24.0 self.timezone = 0 offset = when.utcoffset() if offset is not None: - self.timezone = offset.seconds/3600.0 + self.timezone = offset.seconds / 3600.0 def __calc(self): """ @@ -99,37 +101,56 @@ def __calc(self): sunrise_t, sunset_t and solarnoon_t """ timezone = self.timezone # in hours, east is positive - longitude = self.lng # in decimal degrees, east is positive - latitude = self.lat # in decimal degrees, north is positive + longitude = self.lng # in decimal degrees, east is positive + latitude = self.lat # in decimal degrees, north is positive time = self.time # percentage past midnight, i.e. noon is 0.5 - day = self.day # daynumber 1=1/1/1900 - - jday = day+2415018.5+time-timezone/24 # Julian day - jcent = (jday-2451545)/36525 # Julian century - - manom = 357.52911+jcent*(35999.05029-0.0001537*jcent) - mlong = 280.46646+jcent*(36000.76983+jcent*0.0003032) % 360 - eccent = 0.016708634-jcent*(0.000042037+0.0001537*jcent) - mobliq = 23+(26+((21.448-jcent*(46.815+jcent*(0.00059-jcent*0.001813))))/60)/60 - obliq = mobliq+0.00256*cos(rad(125.04-1934.136*jcent)) - vary = tan(rad(obliq/2))*tan(rad(obliq/2)) - seqcent = sin(rad(manom))*(1.914602-jcent*(0.004817+0.000014*jcent)) +\ - sin(rad(2*manom))*(0.019993-0.000101*jcent)+sin(rad(3*manom))*0.000289 - struelong = mlong+seqcent - sapplong = struelong-0.00569-0.00478*sin(rad(125.04-1934.136*jcent)) - declination = deg(asin(sin(rad(obliq))*sin(rad(sapplong)))) - - eqtime = 4*deg(vary*sin(2*rad(mlong))-2*eccent*sin(rad(manom)) + - 4*eccent*vary*sin(rad(manom))*cos(2*rad(mlong)) - - 0.5*vary*vary*sin(4*rad(mlong))-1.25*eccent*eccent*sin(2*rad(manom))) - - hourangle = deg(acos(cos(rad(90.833))/(cos(rad(latitude)) * - cos(rad(declination)))-tan(rad(latitude))*tan(rad(declination)))) - - self.solarnoon_t = (720-4*longitude-eqtime+timezone*60)/1440 - self.sunrise_t = self.solarnoon_t-hourangle*4/1440 - self.sunset_t = self.solarnoon_t+hourangle*4/1440 + day = self.day # daynumber 1=1/1/1900 + + jday = day + 2415018.5 + time - timezone / 24 # Julian day + jcent = (jday - 2451545) / 36525 # Julian century + + manom = 357.52911 + jcent * (35999.05029 - 0.0001537 * jcent) + mlong = 280.46646 + jcent * (36000.76983 + jcent * 0.0003032) % 360 + eccent = 0.016708634 - jcent * (0.000042037 + 0.0001537 * jcent) + mobliq = ( + 23 + + ( + 26 + + ((21.448 - jcent * (46.815 + jcent * (0.00059 - jcent * 0.001813)))) + / 60 + ) + / 60 + ) + obliq = mobliq + 0.00256 * cos(rad(125.04 - 1934.136 * jcent)) + vary = tan(rad(obliq / 2)) * tan(rad(obliq / 2)) + seqcent = ( + sin(rad(manom)) * (1.914602 - jcent * (0.004817 + 0.000014 * jcent)) + + sin(rad(2 * manom)) * (0.019993 - 0.000101 * jcent) + + sin(rad(3 * manom)) * 0.000289 + ) + struelong = mlong + seqcent + sapplong = struelong - 0.00569 - 0.00478 * sin(rad(125.04 - 1934.136 * jcent)) + declination = deg(asin(sin(rad(obliq)) * sin(rad(sapplong)))) + + eqtime = 4 * deg( + vary * sin(2 * rad(mlong)) + - 2 * eccent * sin(rad(manom)) + + 4 * eccent * vary * sin(rad(manom)) * cos(2 * rad(mlong)) + - 0.5 * vary * vary * sin(4 * rad(mlong)) + - 1.25 * eccent * eccent * sin(2 * rad(manom)) + ) + + hourangle = deg( + acos( + cos(rad(90.833)) / (cos(rad(latitude)) * cos(rad(declination))) + - tan(rad(latitude)) * tan(rad(declination)) + ) + ) + + self.solarnoon_t = (720 - 4 * longitude - eqtime + timezone * 60) / 1440 + self.sunrise_t = self.solarnoon_t - hourangle * 4 / 1440 + self.sunset_t = self.solarnoon_t + hourangle * 4 / 1440 """ diff --git a/weather.metoffice/src/metoffice/constants.py b/weather.metoffice/src/metoffice/constants.py index d1a7f5ee7d..965e0cd8d0 100644 --- a/weather.metoffice/src/metoffice/constants.py +++ b/weather.metoffice/src/metoffice/constants.py @@ -1,200 +1,226 @@ +import urllib.parse + +import pytz import xbmc -import xbmcgui import xbmcaddon +import xbmcgui import xbmcvfs -import urllib.parse -import pytz # Magic numbers. See https://kodi.wiki/view/Window_IDs WEATHER_WINDOW_ID = 12600 ADDON_BROWSER_WINDOW_ID = 10040 -TZ = pytz.timezone('Europe/London') # TODO: Need to pull the actual timezone out of xbmc. Somehow. -TZUK = pytz.timezone('Europe/London') -WINDOW = xbmcgui.Window(WEATHER_WINDOW_ID) -FORECASTMAP_SLIDER = WINDOW.getProperty('ForecastMap.Slider') or '0' -OBSERVATIONMAP_SLIDER = WINDOW.getProperty('ObservationMap.Slider') or '0' -FORECASTMAP_LAYER_SELECTION = WINDOW.getProperty('ForecastMap.LayerSelection') or 'Rainfall' # @UndefinedVariable -OBSERVATIONMAP_LAYER_SELECTION = WINDOW.getProperty('ObservationMap.LayerSelection') or 'Rainfall' # @UndefinedVariable -CURRENT_VIEW = WINDOW.getProperty('Weather.CurrentView') - -ADDON = xbmcaddon.Addon(id="weather.metoffice") -DIALOG = xbmcgui.Dialog() - -KEYBOARD = xbmc.Keyboard() -ADDON_BANNER_PATH = xbmcvfs.translatePath('special://home/addons/%s/resources/banner.png' % ADDON.getAddonInfo('id')) -ADDON_DATA_PATH = xbmcvfs.translatePath('special://profile/addon_data/%s/' % ADDON.getAddonInfo('id')) - -TEMPERATUREUNITS = xbmc.getRegion('tempunit') - -API_KEY = ADDON.getSetting('ApiKey') -GEOLOCATION = ADDON.getSetting('GeoLocation') -GEOIP = ADDON.getSetting('GeoIPProvider') -FORECAST_LOCATION = ADDON.getSetting('ForecastLocation') -FORECAST_LOCATION_ID = ADDON.getSetting('ForecastLocationID') -OBSERVATION_LOCATION = ADDON.getSetting('ObservationLocation') -OBSERVATION_LOCATION_ID = ADDON.getSetting('ObservationLocationID') -REGIONAL_LOCATION = ADDON.getSetting('RegionalLocation') -REGIONAL_LOCATION_ID = ADDON.getSetting('RegionalLocationID') -LATITUDE = ADDON.getSetting('ForecastLocationLatitude') -LONGITUDE = ADDON.getSetting('ForecastLocationLongitude') - -DATAPOINT_DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%S' -DATAPOINT_DATE_FORMAT = '%Y-%m-%dZ' +TZ = pytz.timezone( + "Europe/London" +) # TODO: Need to pull the actual timezone out of xbmc. Somehow. + + +def window(): + return xbmcgui.Window(WEATHER_WINDOW_ID) + + +def dialog(): + return xbmcgui.Dialog() + + +def keyboard(): + return xbmc.Keyboard() + + +def addon(): + return xbmcaddon.Addon(id="weather.metoffice") + + +ADDON_BANNER_PATH = xbmcvfs.translatePath( + "special://home/addons/%s/resources/banner.png" % addon().getAddonInfo("id") +) +ADDON_DATA_PATH = xbmcvfs.translatePath( + "special://profile/addon_data/%s/" % addon().getAddonInfo("id") +) + +TEMPERATUREUNITS = xbmc.getRegion("tempunit") + +API_KEY = addon().getSetting("ApiKey") +GEOLOCATION = addon().getSetting("GeoLocation") +GEOIP = addon().getSetting("GeoIPProvider") +FORECAST_LOCATION = addon().getSetting("ForecastLocation") +FORECAST_LOCATION_ID = addon().getSetting("ForecastLocationID") +OBSERVATION_LOCATION = addon().getSetting("ObservationLocation") +OBSERVATION_LOCATION_ID = addon().getSetting("ObservationLocationID") +REGIONAL_LOCATION = addon().getSetting("RegionalLocation") +REGIONAL_LOCATION_ID = addon().getSetting("RegionalLocationID") +LATITUDE = addon().getSetting("ForecastLocationLatitude") +LONGITUDE = addon().getSetting("ForecastLocationLongitude") + +DATAPOINT_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S" +DATAPOINT_DATE_FORMAT = "%Y-%m-%dZ" SHORT_DAY_FORMAT = "%a" SHORT_DATE_FORMAT = "%d %b" -MAPTIME_FORMAT = '%H%M %a' -ISSUEDAT_FORMAT = '%H:%M %a %d %b %Y' -TIME_FORMAT = '%H:%M' - -GOOGLE_BASE = 'http://maps.googleapis.com/maps/api/staticmap' -GOOGLE_GLOBAL = GOOGLE_BASE + "?sensor=false¢er=55,-3.5&zoom=5&size=323x472" -GOOGLE_SURFACE = GOOGLE_GLOBAL + "&maptype=satellite" -GOOGLE_MARKER = GOOGLE_GLOBAL + \ - '&style=feature:all|element:all|visibility:off&markers={0},{1}'.format(LATITUDE, LONGITUDE) +MAPTIME_FORMAT = "%H%M %a" +ISSUEDAT_FORMAT = "%H:%M %a %d %b %Y" +TIME_FORMAT = "%H:%M" RAW_DATAPOINT_IMG_WIDTH = 500 CROP_WIDTH = 40 CROP_HEIGHT = 20 WEATHER_CODES = { - 'na': ('na', 'Not Available'), - '0': ('31', 'Clear'), # night - '1': ('32', 'Sunny'), - '2': ('29', 'Partly Cloudy'), # night - '3': ('30', 'Partly Cloudy'), + "na": ("na", "Not Available"), + "0": ("31", "Clear"), # night + "1": ("32", "Sunny"), + "2": ("29", "Partly Cloudy"), # night + "3": ("30", "Partly Cloudy"), # '4': ('na', 'Not available'), - '5': ('21', 'Mist'), - '6': ('20', 'Fog'), - '7': ('26', 'Cloudy'), - '8': ('26', 'Overcast'), - '9': ('45', 'Light Rain'), # night - '10': ('11', 'Light Rain'), - '11': ('9', 'Drizzle'), - '12': ('11', 'Light Rain'), - '13': ('45', 'Heavy Rain'), # night - '14': ('40', 'Heavy Rain'), - '15': ('40', 'Heavy Rain'), - '16': ('46', 'Sleet'), # night - '17': ('6', 'Sleet'), - '18': ('6', 'Sleet'), - '19': ('45', 'Hail'), # night - '20': ('18', 'Hail'), - '21': ('18', 'Hail'), - '22': ('46', 'Light Snow'), # night - '23': ('14', 'Light snow'), - '24': ('14', 'Light Snow'), - '25': ('46', 'Heavy Snow'), # night - '26': ('16', 'Heavy Snow'), - '27': ('16', 'Heavy Snow'), - '28': ('47', 'Thunder'), # night - '29': ('17', 'Thunder'), - '30': ('17', 'Thunder') + "5": ("21", "Mist"), + "6": ("20", "Fog"), + "7": ("26", "Cloudy"), + "8": ("26", "Overcast"), + "9": ("45", "Light Rain"), # night + "10": ("11", "Light Rain"), + "11": ("9", "Drizzle"), + "12": ("11", "Light Rain"), + "13": ("45", "Heavy Rain"), # night + "14": ("40", "Heavy Rain"), + "15": ("40", "Heavy Rain"), + "16": ("46", "Sleet"), # night + "17": ("6", "Sleet"), + "18": ("6", "Sleet"), + "19": ("45", "Hail"), # night + "20": ("18", "Hail"), + "21": ("18", "Hail"), + "22": ("46", "Light Snow"), # night + "23": ("14", "Light snow"), + "24": ("14", "Light Snow"), + "25": ("46", "Heavy Snow"), # night + "26": ("16", "Heavy Snow"), + "27": ("16", "Heavy Snow"), + "28": ("47", "Thunder"), # night + "29": ("17", "Thunder"), + "30": ("17", "Thunder"), } # This list must appear in the same order as it appears in # the settings.xml in order for the indexes to align. GEOIP_PROVIDERS = [ - {'url': 'http://ip-api.com/json/', - 'latitude': 'lat', - 'longitude': 'lon'}, - {'url': 'http://freegeoip.net/json/', - 'latitude': 'latitude', - 'longitude': 'longitude'}, - {'url': 'http://geoiplookup.net/geoapi.php?output=json', - 'latitude': 'latitude', - 'longitude': 'longitude'} + {"url": "http://ip-api.com/json/", "latitude": "lat", "longitude": "lon"}, + { + "url": "http://freegeoip.net/json/", + "latitude": "latitude", + "longitude": "longitude", + }, + { + "url": "http://geoiplookup.net/geoapi.php?output=json", + "latitude": "latitude", + "longitude": "longitude", + }, ] GEOIP_PROVIDER = GEOIP_PROVIDERS[int(GEOIP) if GEOIP else 0] URL_TEMPLATE = "http://datapoint.metoffice.gov.uk/public/data/{format}/{resource}/{group}/{datatype}/{object}?{get}" FORECAST_SITELIST_URL = URL_TEMPLATE.format( - format='val', - resource='wxfcs', - group='all', - datatype='json', - object='sitelist', - get=urllib.parse.unquote(urllib.parse.urlencode((('key', API_KEY),)))) + format="val", + resource="wxfcs", + group="all", + datatype="json", + object="sitelist", + get=urllib.parse.unquote(urllib.parse.urlencode((("key", API_KEY),))), +) OBSERVATION_SITELIST_URL = URL_TEMPLATE.format( - format='val', - resource='wxobs', - group='all', - datatype='json', - object='sitelist', - get=urllib.parse.unquote(urllib.parse.urlencode((('key', API_KEY),)))) + format="val", + resource="wxobs", + group="all", + datatype="json", + object="sitelist", + get=urllib.parse.unquote(urllib.parse.urlencode((("key", API_KEY),))), +) REGIONAL_SITELIST_URL = URL_TEMPLATE.format( - format='txt', - resource='wxfcs', - group='regionalforecast', - datatype='json', - object='sitelist', - get=urllib.parse.unquote(urllib.parse.urlencode((('key', API_KEY),)))) + format="txt", + resource="wxfcs", + group="regionalforecast", + datatype="json", + object="sitelist", + get=urllib.parse.unquote(urllib.parse.urlencode((("key", API_KEY),))), +) DAILY_LOCATION_FORECAST_URL = URL_TEMPLATE.format( - format='val', - resource='wxfcs', - group='all', - datatype='json', + format="val", + resource="wxfcs", + group="all", + datatype="json", object=FORECAST_LOCATION_ID, - get=urllib.parse.unquote(urllib.parse.urlencode((('res', 'daily'), ('key', API_KEY))))) + get=urllib.parse.unquote( + urllib.parse.urlencode((("res", "daily"), ("key", API_KEY))) + ), +) THREEHOURLY_LOCATION_FORECAST_URL = URL_TEMPLATE.format( - format='val', - resource='wxfcs', - group='all', - datatype='json', + format="val", + resource="wxfcs", + group="all", + datatype="json", object=FORECAST_LOCATION_ID, - get=urllib.parse.unquote(urllib.parse.urlencode((('res', '3hourly'), ('key', API_KEY))))) + get=urllib.parse.unquote( + urllib.parse.urlencode((("res", "3hourly"), ("key", API_KEY))) + ), +) HOURLY_LOCATION_OBSERVATION_URL = URL_TEMPLATE.format( - format='val', - resource='wxobs', - group='all', - datatype='json', + format="val", + resource="wxobs", + group="all", + datatype="json", object=OBSERVATION_LOCATION_ID, - get=urllib.parse.unquote(urllib.parse.urlencode((('res', 'hourly'), ('key', API_KEY))))) + get=urllib.parse.unquote( + urllib.parse.urlencode((("res", "hourly"), ("key", API_KEY))) + ), +) TEXT_FORECAST_URL = URL_TEMPLATE.format( - format='txt', - resource='wxfcs', - group='regionalforecast', - datatype='json', + format="txt", + resource="wxfcs", + group="regionalforecast", + datatype="json", object=REGIONAL_LOCATION_ID, - get=urllib.parse.unquote(urllib.parse.urlencode((('key', API_KEY),)))) + get=urllib.parse.unquote(urllib.parse.urlencode((("key", API_KEY),))), +) FORECAST_LAYER_CAPABILITIES_URL = URL_TEMPLATE.format( - format='layer', - resource='wxfcs', - group='all', - datatype='json', - object='capabilities', - get=urllib.parse.unquote(urllib.parse.urlencode((('key', API_KEY),)))) + format="layer", + resource="wxfcs", + group="all", + datatype="json", + object="capabilities", + get=urllib.parse.unquote(urllib.parse.urlencode((("key", API_KEY),))), +) OBSERVATION_LAYER_CAPABILITIES_URL = URL_TEMPLATE.format( - format='layer', - resource='wxobs', - group='all', - datatype='json', - object='capabilities', - get=urllib.parse.unquote(urllib.parse.urlencode((('key', API_KEY),)))) - -LONG_REGIONAL_NAMES = {'os': 'Orkney and Shetland', - 'he': 'Highland and Eilean Siar', - 'gr': 'Grampian', - 'ta': 'Tayside', - 'st': 'Strathclyde', - 'dg': 'Dumfries, Galloway, Lothian', - 'ni': 'Northern Ireland', - 'yh': 'Yorkshire and the Humber', - 'ne': 'Northeast England', - 'em': 'East Midlands', - 'ee': 'East of England', - 'se': 'London and Southeast England', - 'nw': 'Northwest England', - 'wm': 'West Midlands', - 'sw': 'Southwest England', - 'wl': 'Wales', - 'uk': 'United Kingdom'} + format="layer", + resource="wxobs", + group="all", + datatype="json", + object="capabilities", + get=urllib.parse.unquote(urllib.parse.urlencode((("key", API_KEY),))), +) + +LONG_REGIONAL_NAMES = { + "os": "Orkney and Shetland", + "he": "Highland and Eilean Siar", + "gr": "Grampian", + "ta": "Tayside", + "st": "Strathclyde", + "dg": "Dumfries, Galloway, Lothian", + "ni": "Northern Ireland", + "yh": "Yorkshire and the Humber", + "ne": "Northeast England", + "em": "East Midlands", + "ee": "East of England", + "se": "London and Southeast England", + "nw": "Northwest England", + "wm": "West Midlands", + "sw": "Southwest England", + "wl": "Wales", + "uk": "United Kingdom", +} diff --git a/weather.metoffice/src/metoffice/properties.py b/weather.metoffice/src/metoffice/properties.py index ae17254234..76164986e9 100644 --- a/weather.metoffice/src/metoffice/properties.py +++ b/weather.metoffice/src/metoffice/properties.py @@ -1,447 +1,374 @@ +import json import time +from datetime import timedelta + import pytz -from datetime import datetime, timedelta -import json -from PIL import Image -from . import astronomy -from . import utilities -from . import urlcache -from .constants import ISSUEDAT_FORMAT, DATAPOINT_DATETIME_FORMAT,\ - SHORT_DAY_FORMAT, SHORT_DATE_FORMAT, TIME_FORMAT, DATAPOINT_DATE_FORMAT,\ - WEATHER_CODES, WINDOW, DAILY_LOCATION_FORECAST_URL,\ - API_KEY, ADDON_DATA_PATH, THREEHOURLY_LOCATION_FORECAST_URL,\ - TEXT_FORECAST_URL, HOURLY_LOCATION_OBSERVATION_URL,\ - FORECAST_LAYER_CAPABILITIES_URL, OBSERVATION_LAYER_CAPABILITIES_URL,\ - RAW_DATAPOINT_IMG_WIDTH, CROP_WIDTH, CROP_HEIGHT, GOOGLE_SURFACE,\ - GOOGLE_MARKER, MAPTIME_FORMAT, REGIONAL_LOCATION, REGIONAL_LOCATION_ID,\ - FORECAST_LOCATION, FORECAST_LOCATION_ID, OBSERVATION_LOCATION,\ - OBSERVATION_LOCATION_ID, FORECASTMAP_SLIDER, OBSERVATIONMAP_SLIDER,\ - FORECASTMAP_LAYER_SELECTION, OBSERVATIONMAP_LAYER_SELECTION, TZ, TZUK,\ - TEMPERATUREUNITS, LATITUDE, LONGITUDE +from . import astronomy, urlcache, utilities +from .constants import ( + ADDON_DATA_PATH, + DAILY_LOCATION_FORECAST_URL, + DATAPOINT_DATE_FORMAT, + DATAPOINT_DATETIME_FORMAT, + FORECAST_LOCATION, + FORECAST_LOCATION_ID, + HOURLY_LOCATION_OBSERVATION_URL, + ISSUEDAT_FORMAT, + LATITUDE, + LONGITUDE, + OBSERVATION_LOCATION, + OBSERVATION_LOCATION_ID, + REGIONAL_LOCATION, + REGIONAL_LOCATION_ID, + SHORT_DATE_FORMAT, + SHORT_DAY_FORMAT, + TEMPERATUREUNITS, + TEXT_FORECAST_URL, + THREEHOURLY_LOCATION_FORECAST_URL, + TIME_FORMAT, + TZ, + WEATHER_CODES, + window, +) def observation(): - utilities.log("Fetching Hourly Observation for '%s (%s)' from the Met Office..." % ( - OBSERVATION_LOCATION, OBSERVATION_LOCATION_ID)) + utilities.log( + "Fetching Hourly Observation for '%s (%s)' from the Met Office..." + % (OBSERVATION_LOCATION, OBSERVATION_LOCATION_ID) + ) with urlcache.URLCache(ADDON_DATA_PATH) as cache: - filename = cache.get( - HOURLY_LOCATION_OBSERVATION_URL, observation_expiry) + filename = cache.get(HOURLY_LOCATION_OBSERVATION_URL, observation_expiry) with open(filename) as fh: data = json.load(fh) try: - dv = data['SiteRep']['DV'] - dataDate = utilities.strptime(dv.get('dataDate').rstrip( - 'Z'), DATAPOINT_DATETIME_FORMAT).replace(tzinfo=pytz.utc) - WINDOW.setProperty('HourlyObservation.IssuedAt', dataDate.astimezone( - TZ).strftime(ISSUEDAT_FORMAT)) + dv = data["SiteRep"]["DV"] + dataDate = utilities.strptime( + dv.get("dataDate").rstrip("Z"), DATAPOINT_DATETIME_FORMAT + ).replace(tzinfo=pytz.utc) + window().setProperty( + "HourlyObservation.IssuedAt", + dataDate.astimezone(TZ).strftime(ISSUEDAT_FORMAT), + ) try: - latest_period = dv['Location']['Period'][-1] + latest_period = dv["Location"]["Period"][-1] except KeyError: - latest_period = dv['Location']['Period'] + latest_period = dv["Location"]["Period"] try: - latest_obs = latest_period['Rep'][-1] + latest_obs = latest_period["Rep"][-1] except KeyError: - latest_obs = latest_period['Rep'] - WINDOW.setProperty('Current.Condition', WEATHER_CODES[latest_obs.get( - 'W', 'na')][1]) - WINDOW.setProperty('Current.Visibility', latest_obs.get( - 'V', 'n/a')) - WINDOW.setProperty('Current.Pressure', latest_obs.get( - 'P', 'n/a')) - WINDOW.setProperty('Current.Temperature', str( - round(float(latest_obs.get('T', 'n/a')))).split('.')[0]) - WINDOW.setProperty('Current.FeelsLike', 'n/a') + latest_obs = latest_period["Rep"] + window().setProperty( + "Current.Condition", WEATHER_CODES[latest_obs.get("W", "na")][1] + ) + window().setProperty("Current.Visibility", latest_obs.get("V", "n/a")) + window().setProperty("Current.Pressure", latest_obs.get("P", "n/a")) + window().setProperty( + "Current.Temperature", + str(round(float(latest_obs.get("T", "n/a")))).split(".")[0], + ) + window().setProperty("Current.FeelsLike", "n/a") # if we get Wind, then convert it to kmph. - WINDOW.setProperty('Current.Wind', utilities.mph_to_kmph( - latest_obs, 'S')) - WINDOW.setProperty('Current.WindDirection', latest_obs.get( - 'D', 'n/a')) - WINDOW.setProperty('Current.WindGust', latest_obs.get( - 'G', 'n/a')) - WINDOW.setProperty('Current.OutlookIcon', '%s.png' % - WEATHER_CODES[latest_obs.get('W', 'na')][0]) - WINDOW.setProperty('Current.FanartCode', '%s.png' % WEATHER_CODES[latest_obs.get( - 'W', 'na')][0]) - WINDOW.setProperty('Current.DewPoint', str( - round(float(latest_obs.get('Dp', 'n/a')))).split('.')[0]) - WINDOW.setProperty('Current.Humidity', str( - round(float(latest_obs.get('H', 'n/a')))).split('.')[0]) - - WINDOW.setProperty('HourlyObservation.IsFetched', - 'true') + window().setProperty("Current.Wind", utilities.mph_to_kmph(latest_obs, "S")) + window().setProperty("Current.WindDirection", latest_obs.get("D", "n/a")) + window().setProperty("Current.WindGust", latest_obs.get("G", "n/a")) + window().setProperty( + "Current.OutlookIcon", + "%s.png" % WEATHER_CODES[latest_obs.get("W", "na")][0], + ) + window().setProperty( + "Current.FanartCode", "%s.png" % WEATHER_CODES[latest_obs.get("W", "na")][0] + ) + window().setProperty( + "Current.DewPoint", + str(round(float(latest_obs.get("Dp", "n/a")))).split(".")[0], + ) + window().setProperty( + "Current.Humidity", + str(round(float(latest_obs.get("H", "n/a")))).split(".")[0], + ) + + window().setProperty("HourlyObservation.IsFetched", "true") except KeyError as e: - e.args = ("Key Error in JSON File", "Key '{0}' not found while processing file from url:".format( - e.args[0]), HOURLY_LOCATION_OBSERVATION_URL) + e.args = ( + "Key Error in JSON File", + "Key '{0}' not found while processing file from url:".format(e.args[0]), + HOURLY_LOCATION_OBSERVATION_URL, + ) raise def daily(): - utilities.log("Fetching Daily Forecast for '%s (%s)' from the Met Office..." % ( - FORECAST_LOCATION, FORECAST_LOCATION_ID)) + utilities.log( + "Fetching Daily Forecast for '%s (%s)' from the Met Office..." + % (FORECAST_LOCATION, FORECAST_LOCATION_ID) + ) with urlcache.URLCache(ADDON_DATA_PATH) as cache: filename = cache.get(DAILY_LOCATION_FORECAST_URL, daily_expiry) with open(filename) as fh: data = json.load(fh) try: - dv = data['SiteRep']['DV'] - dataDate = utilities.strptime(dv.get('dataDate').rstrip( - 'Z'), DATAPOINT_DATETIME_FORMAT).replace(tzinfo=pytz.utc) - WINDOW.setProperty('DailyForecast.IssuedAt', dataDate.astimezone( - TZ).strftime(ISSUEDAT_FORMAT)) - for p, period in enumerate(dv['Location']['Period']): - WINDOW.setProperty('Day%d.Title' % p, time.strftime(SHORT_DAY_FORMAT, time.strptime( - period.get('value'), DATAPOINT_DATE_FORMAT))) - WINDOW.setProperty('Daily.%d.ShortDay' % (p+1), time.strftime(SHORT_DAY_FORMAT, - time.strptime(period.get('value'), DATAPOINT_DATE_FORMAT))) - WINDOW.setProperty('Daily.%d.ShortDate' % (p+1), time.strftime(SHORT_DATE_FORMAT, - time.strptime(period.get('value'), DATAPOINT_DATE_FORMAT))) - for rep in period['Rep']: - weather_type = rep.get('W', 'na') - if rep.get('$') == 'Day': - WINDOW.setProperty('Day%d.HighTemp' % p, rep.get( - 'Dm', 'na')) - WINDOW.setProperty('Day%d.HighTempIcon' % - p, rep.get('Dm')) - WINDOW.setProperty('Day%d.Outlook' % p, WEATHER_CODES.get( - weather_type)[1]) - WINDOW.setProperty('Day%d.OutlookIcon' % p, "%s.png" % WEATHER_CODES.get( - weather_type, 'na')[0]) - WINDOW.setProperty('Day%d.WindSpeed' % p, rep.get( - 'S', 'na')) - WINDOW.setProperty('Day%d.WindDirection' % p, rep.get( - 'D', 'na').lower()) + dv = data["SiteRep"]["DV"] + dataDate = utilities.strptime( + dv.get("dataDate").rstrip("Z"), DATAPOINT_DATETIME_FORMAT + ).replace(tzinfo=pytz.utc) + window().setProperty( + "DailyForecast.IssuedAt", dataDate.astimezone(TZ).strftime(ISSUEDAT_FORMAT) + ) + for p, period in enumerate(dv["Location"]["Period"]): + window().setProperty( + "Day%d.Title" % p, + time.strftime( + SHORT_DAY_FORMAT, + time.strptime(period.get("value"), DATAPOINT_DATE_FORMAT), + ), + ) + window().setProperty( + "Daily.%d.ShortDay" % (p + 1), + time.strftime( + SHORT_DAY_FORMAT, + time.strptime(period.get("value"), DATAPOINT_DATE_FORMAT), + ), + ) + window().setProperty( + "Daily.%d.ShortDate" % (p + 1), + time.strftime( + SHORT_DATE_FORMAT, + time.strptime(period.get("value"), DATAPOINT_DATE_FORMAT), + ), + ) + for rep in period["Rep"]: + weather_type = rep.get("W", "na") + if rep.get("$") == "Day": + window().setProperty("Day%d.HighTemp" % p, rep.get("Dm", "na")) + window().setProperty("Day%d.HighTempIcon" % p, rep.get("Dm")) + window().setProperty( + "Day%d.Outlook" % p, WEATHER_CODES.get(weather_type)[1] + ) + window().setProperty( + "Day%d.OutlookIcon" % p, + "%s.png" % WEATHER_CODES.get(weather_type, "na")[0], + ) + window().setProperty("Day%d.WindSpeed" % p, rep.get("S", "na")) + window().setProperty( + "Day%d.WindDirection" % p, rep.get("D", "na").lower() + ) # "Extended" properties used by some skins. - WINDOW.setProperty('Daily.%d.HighTemperature' % ( - p+1), utilities.localised_temperature(rep.get('Dm', 'na'))+TEMPERATUREUNITS) - WINDOW.setProperty('Daily.%d.HighTempIcon' % ( - p+1), rep.get('Dm')) - WINDOW.setProperty('Daily.%d.Outlook' % ( - p+1), WEATHER_CODES.get(weather_type)[1]) - WINDOW.setProperty('Daily.%d.OutlookIcon' % ( - p+1), "%s.png" % WEATHER_CODES.get(weather_type, 'na')[0]) - WINDOW.setProperty('Daily.%d.FanartCode' % ( - p+1), WEATHER_CODES.get(weather_type, 'na')[0]) - WINDOW.setProperty('Daily.%d.WindSpeed' % ( - p+1), rep.get('S', 'na')) - WINDOW.setProperty('Daily.%d.WindDirection' % ( - p+1), rep.get('D', 'na').lower()) - - elif rep.get('$') == 'Night': - WINDOW.setProperty('Day%d.LowTemp' % p, rep.get( - 'Nm', 'na')) - WINDOW.setProperty('Day%d.LowTempIcon' % - p, rep.get('Nm')) - - WINDOW.setProperty('Daily.%d.LowTemperature' % ( - p+1), utilities.localised_temperature(rep.get('Nm', 'na'))+TEMPERATUREUNITS) - WINDOW.setProperty('Daily.%d.LowTempIcon' % ( - p+1), rep.get('Nm')) + window().setProperty( + "Daily.%d.HighTemperature" % (p + 1), + utilities.localised_temperature(rep.get("Dm", "na")) + + TEMPERATUREUNITS, + ) + window().setProperty( + "Daily.%d.HighTempIcon" % (p + 1), rep.get("Dm") + ) + window().setProperty( + "Daily.%d.Outlook" % (p + 1), WEATHER_CODES.get(weather_type)[1] + ) + window().setProperty( + "Daily.%d.OutlookIcon" % (p + 1), + "%s.png" % WEATHER_CODES.get(weather_type, "na")[0], + ) + window().setProperty( + "Daily.%d.FanartCode" % (p + 1), + WEATHER_CODES.get(weather_type, "na")[0], + ) + window().setProperty( + "Daily.%d.WindSpeed" % (p + 1), rep.get("S", "na") + ) + window().setProperty( + "Daily.%d.WindDirection" % (p + 1), rep.get("D", "na").lower() + ) + + elif rep.get("$") == "Night": + window().setProperty("Day%d.LowTemp" % p, rep.get("Nm", "na")) + window().setProperty("Day%d.LowTempIcon" % p, rep.get("Nm")) + + window().setProperty( + "Daily.%d.LowTemperature" % (p + 1), + utilities.localised_temperature(rep.get("Nm", "na")) + + TEMPERATUREUNITS, + ) + window().setProperty( + "Daily.%d.LowTempIcon" % (p + 1), rep.get("Nm") + ) except KeyError as e: - e.args = ("Key Error in JSON File", "Key '{0}' not found while processing file from url:".format( - e.args[0]), DAILY_LOCATION_FORECAST_URL) + e.args = ( + "Key Error in JSON File", + "Key '{0}' not found while processing file from url:".format(e.args[0]), + DAILY_LOCATION_FORECAST_URL, + ) raise - WINDOW.setProperty('Daily.IsFetched', 'true') + window().setProperty("Daily.IsFetched", "true") def threehourly(): - utilities.log("Fetching 3 Hourly Forecast for '%s (%s)' from the Met Office..." % ( - FORECAST_LOCATION, FORECAST_LOCATION_ID)) + utilities.log( + "Fetching 3 Hourly Forecast for '%s (%s)' from the Met Office..." + % (FORECAST_LOCATION, FORECAST_LOCATION_ID) + ) with urlcache.URLCache(ADDON_DATA_PATH) as cache: - filename = cache.get( - THREEHOURLY_LOCATION_FORECAST_URL, threehourly_expiry) + filename = cache.get(THREEHOURLY_LOCATION_FORECAST_URL, threehourly_expiry) with open(filename) as fh: data = json.load(fh) try: - dv = data['SiteRep']['DV'] - dataDate = utilities.strptime(dv.get('dataDate').rstrip( - 'Z'), DATAPOINT_DATETIME_FORMAT).replace(tzinfo=pytz.utc) - WINDOW.setProperty('3HourlyForecast.IssuedAt', dataDate.astimezone( - TZ).strftime(ISSUEDAT_FORMAT)) + dv = data["SiteRep"]["DV"] + dataDate = utilities.strptime( + dv.get("dataDate").rstrip("Z"), DATAPOINT_DATETIME_FORMAT + ).replace(tzinfo=pytz.utc) + window().setProperty( + "3HourlyForecast.IssuedAt", + dataDate.astimezone(TZ).strftime(ISSUEDAT_FORMAT), + ) count = 1 - for period in dv['Location']['Period']: - for rep in period['Rep']: + for period in dv["Location"]["Period"]: + for rep in period["Rep"]: # extra xbmc targeted info: - weather_type = rep.get('W', 'na') - WINDOW.setProperty('Hourly.%d.Outlook' % count, WEATHER_CODES.get( - weather_type)[1]) - WINDOW.setProperty('Hourly.%d.WindSpeed' % count, rep.get( - 'S', 'n/a')) - WINDOW.setProperty('Hourly.%d.WindDirection' % count, rep.get( - 'D', 'na').lower()) - WINDOW.setProperty('Hourly.%d.GustSpeed' % count, rep.get( - 'G', 'n/a')) - WINDOW.setProperty('Hourly.%d.UVIndex' % count, rep.get( - 'U', 'n/a')) - WINDOW.setProperty('Hourly.%d.Precipitation' % - count, rep.get('Pp') + "%") - WINDOW.setProperty('Hourly.%d.OutlookIcon' % count, "%s.png" % WEATHER_CODES.get( - weather_type, 'na')[0]) - WINDOW.setProperty('Hourly.%d.ShortDate' % count, time.strftime(SHORT_DATE_FORMAT, time.strptime( - period.get('value'), DATAPOINT_DATE_FORMAT))) - WINDOW.setProperty('Hourly.%d.Time' % count, utilities.minutes_as_time( - int(rep.get('$')))) - WINDOW.setProperty('Hourly.%d.Temperature' % count, utilities.rownd( - utilities.localised_temperature(rep.get('T', 'na')))+TEMPERATUREUNITS) - WINDOW.setProperty('Hourly.%d.ActualTempIcon' % - count, rep.get('T', 'na')) - WINDOW.setProperty('Hourly.%d.FeelsLikeTemp' % count, utilities.rownd( - utilities.localised_temperature(rep.get('F', 'na')))) - WINDOW.setProperty('Hourly.%d.FeelsLikeTempIcon' % - count, rep.get('F', 'na')) + weather_type = rep.get("W", "na") + window().setProperty( + "Hourly.%d.Outlook" % count, WEATHER_CODES.get(weather_type)[1] + ) + window().setProperty("Hourly.%d.WindSpeed" % count, rep.get("S", "n/a")) + window().setProperty( + "Hourly.%d.WindDirection" % count, rep.get("D", "na").lower() + ) + window().setProperty("Hourly.%d.GustSpeed" % count, rep.get("G", "n/a")) + window().setProperty("Hourly.%d.UVIndex" % count, rep.get("U", "n/a")) + window().setProperty( + "Hourly.%d.Precipitation" % count, rep.get("Pp") + "%" + ) + window().setProperty( + "Hourly.%d.OutlookIcon" % count, + "%s.png" % WEATHER_CODES.get(weather_type, "na")[0], + ) + window().setProperty( + "Hourly.%d.ShortDate" % count, + time.strftime( + SHORT_DATE_FORMAT, + time.strptime(period.get("value"), DATAPOINT_DATE_FORMAT), + ), + ) + window().setProperty( + "Hourly.%d.Time" % count, + utilities.minutes_as_time(int(rep.get("$"))), + ) + window().setProperty( + "Hourly.%d.Temperature" % count, + utilities.rownd(utilities.localised_temperature(rep.get("T", "na"))) + + TEMPERATUREUNITS, + ) + window().setProperty( + "Hourly.%d.ActualTempIcon" % count, rep.get("T", "na") + ) + window().setProperty( + "Hourly.%d.FeelsLikeTemp" % count, + utilities.rownd( + utilities.localised_temperature(rep.get("F", "na")) + ), + ) + window().setProperty( + "Hourly.%d.FeelsLikeTempIcon" % count, rep.get("F", "na") + ) count += 1 except KeyError as e: - e.args = ("Key Error in JSON File", "Key '{0}' not found while processing file from url:".format( - e.args[0]), THREEHOURLY_LOCATION_FORECAST_URL) + e.args = ( + "Key Error in JSON File", + "Key '{0}' not found while processing file from url:".format(e.args[0]), + THREEHOURLY_LOCATION_FORECAST_URL, + ) raise - WINDOW.setProperty('Hourly.IsFetched', 'true') + window().setProperty("Hourly.IsFetched", "true") def sunrisesunset(): sun = astronomy.Sun(lat=float(LATITUDE), lng=float(LONGITUDE)) - WINDOW.setProperty('Today.Sunrise', sun.sunrise().strftime(TIME_FORMAT)) - WINDOW.setProperty('Today.Sunset', sun.sunset().strftime(TIME_FORMAT)) + window().setProperty("Today.Sunrise", sun.sunrise().strftime(TIME_FORMAT)) + window().setProperty("Today.Sunset", sun.sunset().strftime(TIME_FORMAT)) def text(): - utilities.log("Fetching Text Forecast for '%s (%s)' from the Met Office..." % ( - REGIONAL_LOCATION, REGIONAL_LOCATION_ID)) + utilities.log( + "Fetching Text Forecast for '%s (%s)' from the Met Office..." + % (REGIONAL_LOCATION, REGIONAL_LOCATION_ID) + ) with urlcache.URLCache(ADDON_DATA_PATH) as cache: filename = cache.get(TEXT_FORECAST_URL, text_expiry) with open(filename) as fh: data = json.load(fh) try: - rf = data['RegionalFcst'] - issuedat = utilities.strptime(rf['issuedAt'].rstrip( - 'Z'), DATAPOINT_DATETIME_FORMAT).replace(tzinfo=pytz.utc) - WINDOW.setProperty('TextForecast.IssuedAt', issuedat.astimezone( - TZ).strftime(ISSUEDAT_FORMAT)) + rf = data["RegionalFcst"] + issuedat = utilities.strptime( + rf["issuedAt"].rstrip("Z"), DATAPOINT_DATETIME_FORMAT + ).replace(tzinfo=pytz.utc) + window().setProperty( + "TextForecast.IssuedAt", issuedat.astimezone(TZ).strftime(ISSUEDAT_FORMAT) + ) count = 0 - for period in rf['FcstPeriods']['Period']: + for period in rf["FcstPeriods"]["Period"]: # have to check type because json can return list or dict here - if isinstance(period['Paragraph'], list): - for paragraph in period['Paragraph']: - WINDOW.setProperty('Text.Paragraph%d.Title' % count, paragraph['title'].rstrip( - ':').lstrip('UK Outlook for')) - WINDOW.setProperty('Text.Paragraph%d.Content' % - count, paragraph['$']) + if isinstance(period["Paragraph"], list): + for paragraph in period["Paragraph"]: + window().setProperty( + "Text.Paragraph%d.Title" % count, + paragraph["title"].rstrip(":").lstrip("UK Outlook for"), + ) + window().setProperty( + "Text.Paragraph%d.Content" % count, paragraph["$"] + ) count += 1 else: - WINDOW.setProperty('Text.Paragraph%d.Title' % count, period['Paragraph']['title'].rstrip( - ':').lstrip('UK Outlook for')) - - WINDOW.setProperty('Text.Paragraph%d.Content' % - count, period['Paragraph']['$']) + window().setProperty( + "Text.Paragraph%d.Title" % count, + period["Paragraph"]["title"].rstrip(":").lstrip("UK Outlook for"), + ) + + window().setProperty( + "Text.Paragraph%d.Content" % count, period["Paragraph"]["$"] + ) count += 1 except KeyError as e: - e.args = ("Key Error in JSON File", "Key '{0}' not found while processing file from url:".format( - e.args[0]), TEXT_FORECAST_URL) + e.args = ( + "Key Error in JSON File", + "Key '{0}' not found while processing file from url:".format(e.args[0]), + TEXT_FORECAST_URL, + ) raise - WINDOW.setProperty('TextForecast.IsFetched', 'true') - - -def forecastlayer(): - utilities.log("Fetching '{0}' Forecast Map with index '{1}'...".format( - FORECASTMAP_LAYER_SELECTION, FORECASTMAP_SLIDER)) - with urlcache.URLCache(ADDON_DATA_PATH) as cache: - surface = cache.get( - GOOGLE_SURFACE, lambda x: datetime.utcnow() + timedelta(days=30)) - marker = cache.get( - GOOGLE_MARKER, lambda x: datetime.utcnow() + timedelta(days=30)) - - filename = cache.get(FORECAST_LAYER_CAPABILITIES_URL, - forecastlayer_capabilities_expiry) - with open(filename) as fh: - data = json.load(fh) - # pull parameters out of capabilities file - TODO: consider using jsonpath here - try: - for thislayer in data['Layers']['Layer']: - if thislayer['@displayName'] == FORECASTMAP_LAYER_SELECTION: - layer_name = thislayer['Service']['LayerName'] - image_format = thislayer['Service']['ImageFormat'] - default_time = thislayer['Service']['Timesteps']['@defaultTime'] - timesteps = thislayer['Service']['Timesteps']['Timestep'] - break - else: - raise Exception('Error', "Couldn't find layer '%s'" % - FORECASTMAP_LAYER_SELECTION) - except KeyError as e: - e.args = ("Key Error in JSON File", "Key '{0}' not found while processing file from url:".format( - e.args[0]), FORECAST_LAYER_CAPABILITIES_URL) - raise - - issuedat = utilities.strptime( - default_time, DATAPOINT_DATETIME_FORMAT).replace(tzinfo=pytz.utc) - - index = FORECASTMAP_SLIDER - if int(index) < 0: - utilities.log('Slider is negative. Fetching with index 0') - WINDOW.setProperty('ForecastMap.Slider', '0') - index = '0' - elif int(index) > len(timesteps)-1: - utilities.log('Slider exceeds available index range. Fetching with index {0}'.format( - str(len(timesteps)-1))) - WINDOW.setProperty('ForecastMap.Slider', str( - len(timesteps)-1)) - index = str(len(timesteps)-1) - - timestep = timesteps[int(index)] - delta = timedelta(hours=timestep) - maptime = TZUK.normalize(issuedat + delta) - - # get overlay using parameters from gui settings - try: - LayerURL = data['Layers']['BaseUrl']['$'] - except KeyError as e: - e.args = ("Key Error in JSON File", "Key '{0}' not found while processing file from url:".format( - e.args[0]), FORECAST_LAYER_CAPABILITIES_URL) - raise - - url = LayerURL.format(LayerName=layer_name, - ImageFormat=image_format, - DefaultTime=default_time, - Timestep=timestep, - key=API_KEY) - layer = cache.get(url, lambda x: datetime.utcnow() + - timedelta(days=1), image_resize) - - WINDOW.setProperty('ForecastMap.Surface', - surface) - WINDOW.setProperty('ForecastMap.Marker', marker) - WINDOW.setProperty('ForecastMap.IssuedAt', issuedat.astimezone( - TZ).strftime(ISSUEDAT_FORMAT)) - WINDOW.setProperty('ForecastMap.MapTime', maptime.strftime( - MAPTIME_FORMAT)) - WINDOW.setProperty('ForecastMap.Layer', layer) - WINDOW.setProperty('ForecastMap.IsFetched', - 'true') - - -def observationlayer(): - utilities.log("Fetching '{0}' Observation Map with index '{1}'...".format( - OBSERVATIONMAP_LAYER_SELECTION, OBSERVATIONMAP_SLIDER)) - - with urlcache.URLCache(ADDON_DATA_PATH) as cache: - surface = cache.get( - GOOGLE_SURFACE, lambda x: datetime.utcnow() + timedelta(days=30)) - marker = cache.get( - GOOGLE_MARKER, lambda x: datetime.utcnow() + timedelta(days=30)) - - filename = cache.get(OBSERVATION_LAYER_CAPABILITIES_URL, - observationlayer_capabilities_expiry) - with open(filename) as fh: - data = json.load(fh) - # pull parameters out of capabilities file - TODO: consider using jsonpath here - try: - issued_at = data['Layers']['Layer'][-1]['Service']['Times']['Time'][0] - issuedat = utilities.strptime(issued_at, DATAPOINT_DATETIME_FORMAT).replace(tzinfo=pytz.utc) - for thislayer in data['Layers']['Layer']: - if thislayer['@displayName'] == OBSERVATIONMAP_LAYER_SELECTION: - layer_name = thislayer['Service']['LayerName'] - image_format = thislayer['Service']['ImageFormat'] - times = thislayer['Service']['Times']['Time'] - break - else: - raise Exception('Error', "Couldn't find layer '%s'" % - OBSERVATIONMAP_LAYER_SELECTION) - except KeyError as e: - e.args = ("Key Error in JSON File", "Key '{0}' not found while processing file from url:".format( - e.args[0]), OBSERVATION_LAYER_CAPABILITIES_URL) - raise - - index = OBSERVATIONMAP_SLIDER - if int(index) < 0: - utilities.log('Slider is negative. Fetching with index 0') - WINDOW.setProperty('ObservationMap.Slider', - '0') - index = '0' - elif int(index) > len(times)-1: - utilities.log('Slider exceeds available index range. Fetching with index {0}'.format( - str(len(times)-1))) - WINDOW.setProperty('ObservationMap.Slider', str( - len(times)-1)) - index = str(len(times)-1) - - indexedtime = times[int(index)] - maptime = utilities.strptime( - indexedtime, DATAPOINT_DATETIME_FORMAT).replace(tzinfo=pytz.utc) - - # get overlay using parameters from gui settings - try: - LayerURL = data['Layers']['BaseUrl']['$'] - except KeyError as e: - e.args = ("Key Error in JSON File", "Key '{0}' not found while processing file from url:".format( - e.args[0]), OBSERVATION_LAYER_CAPABILITIES_URL) - raise - - url = LayerURL.format(LayerName=layer_name, - ImageFormat=image_format, - Time=indexedtime, - key=API_KEY) - layer = cache.get(url, lambda x: datetime.utcnow() + - timedelta(days=1), image_resize) - - WINDOW.setProperty('ObservationMap.Surface', surface) - WINDOW.setProperty('ObservationMap.Marker', marker) - WINDOW.setProperty('ObservationMap.IssuedAt', issuedat.astimezone( - TZ).strftime(ISSUEDAT_FORMAT)) - WINDOW.setProperty('ObservationMap.MapTime', maptime.astimezone( - TZUK).strftime(MAPTIME_FORMAT)) - WINDOW.setProperty('ObservationMap.Layer', layer) - WINDOW.setProperty('ObservationMap.IsFetched', 'true') + window().setProperty("TextForecast.IsFetched", "true") def daily_expiry(filename): with open(filename) as fh: data = json.load(fh) - dataDate = data['SiteRep']['DV']['dataDate'].rstrip('Z') - return utilities.strptime(dataDate, DATAPOINT_DATETIME_FORMAT) + timedelta(hours=1.5) + dataDate = data["SiteRep"]["DV"]["dataDate"].rstrip("Z") + return utilities.strptime(dataDate, DATAPOINT_DATETIME_FORMAT) + timedelta( + hours=1.5 + ) def threehourly_expiry(filename): with open(filename) as fh: data = json.load(fh) - dataDate = data['SiteRep']['DV']['dataDate'].rstrip('Z') - return utilities.strptime(dataDate, DATAPOINT_DATETIME_FORMAT) + timedelta(hours=1.5) + dataDate = data["SiteRep"]["DV"]["dataDate"].rstrip("Z") + return utilities.strptime(dataDate, DATAPOINT_DATETIME_FORMAT) + timedelta( + hours=1.5 + ) def text_expiry(filename): with open(filename) as fh: data = json.load(fh) - issuedAt = data['RegionalFcst']['issuedAt'].rstrip('Z') + issuedAt = data["RegionalFcst"]["issuedAt"].rstrip("Z") return utilities.strptime(issuedAt, DATAPOINT_DATETIME_FORMAT) + timedelta(hours=12) def observation_expiry(filename): with open(filename) as fh: data = json.load(fh) - dataDate = data['SiteRep']['DV']['dataDate'].rstrip('Z') - return utilities.strptime(dataDate, DATAPOINT_DATETIME_FORMAT) + timedelta(hours=1.5) - - -def forecastlayer_capabilities_expiry(filename): - with open(filename) as fh: - data = json.load(fh) - defaultTime = data['Layers']['Layer'][0]['Service']['Timesteps']['@defaultTime'] - return utilities.strptime(defaultTime, DATAPOINT_DATETIME_FORMAT) + timedelta(hours=9) - - -def observationlayer_capabilities_expiry(filename): - # TODO: Assumes 'Rainfall' is the last report in the file, and Rainfall is the best indicator of issue time - with open(filename) as fh: - data = json.load(fh) - tyme = data['Layers']['Layer'][-1]['Service']['Times']['Time'][0] - return utilities.strptime(tyme, DATAPOINT_DATETIME_FORMAT) + timedelta(minutes=30) - - -def image_resize(filename): - # remove the 'cone' from the image - with Image.open(filename) as img: - (width, height) = img.size - if width == RAW_DATAPOINT_IMG_WIDTH: - img.crop((CROP_WIDTH, CROP_HEIGHT, width-CROP_WIDTH, - height-CROP_HEIGHT)).save(filename, img.format) + dataDate = data["SiteRep"]["DV"]["dataDate"].rstrip("Z") + return utilities.strptime(dataDate, DATAPOINT_DATETIME_FORMAT) + timedelta( + hours=1.5 + ) diff --git a/weather.metoffice/src/metoffice/urlcache.py b/weather.metoffice/src/metoffice/urlcache.py index 26e9d055b4..3c8a5d77a3 100644 --- a/weather.metoffice/src/metoffice/urlcache.py +++ b/weather.metoffice/src/metoffice/urlcache.py @@ -1,34 +1,34 @@ # A basic way of caching files associated with URLs -from datetime import datetime -import os -import urllib.request -import tempfile import json -import socket +import os import shutil +import socket +import tempfile +import urllib.request +from datetime import datetime from . import utilities -throwaway = utilities.strptime('20170101', '%Y%m%d') +throwaway = utilities.strptime("20170101", "%Y%m%d") class URLCache(object): - TIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ' + TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" def __init__(self, folder): - self._folder = os.path.join(folder, 'cache') - self._file = os.path.join(folder, 'cache.json') + self._folder = os.path.join(folder, "cache") + self._file = os.path.join(folder, "cache.json") def __enter__(self): if not os.path.exists(self._folder): os.makedirs(self._folder) try: - fyle = open(self._file, 'r') + fyle = open(self._file, "r") except IOError: # create the file and try again. - open(self._file, 'a').close() - fyle = open(self._file, 'r') + open(self._file, "a").close() + fyle = open(self._file, "r") try: self._cache = json.load(fyle) except ValueError: @@ -38,21 +38,24 @@ def __enter__(self): def __exit__(self, typ, value, traceback): self.flush() - with open(self._file, 'w+') as fyle: + with open(self._file, "w+") as fyle: json.dump(self._cache, fyle, indent=2) def remove(self, url): if url in self._cache: entry = self._cache[url] - if os.path.isfile(entry['resource']): - os.remove(entry['resource']) + if os.path.isfile(entry["resource"]): + os.remove(entry["resource"]) del self._cache[url] def flush(self): flushlist = list() for url, entry in self._cache.items(): - if not os.path.isfile(entry['resource']) or \ - utilities.strptime(entry['expiry'], self.TIME_FORMAT) < datetime.utcnow(): + if ( + not os.path.isfile(entry["resource"]) + or utilities.strptime(entry["expiry"], self.TIME_FORMAT) + < datetime.utcnow() + ): flushlist.append(url) for url in flushlist: self.remove(url) @@ -67,15 +70,18 @@ def get(self, url, expiry_callback, resource_callback=None): """ try: entry = self._cache[url] - if not os.path.isfile(entry['resource']) or \ - utilities.strptime(entry['expiry'], self.TIME_FORMAT) < datetime.utcnow(): + if ( + not os.path.isfile(entry["resource"]) + or utilities.strptime(entry["expiry"], self.TIME_FORMAT) + < datetime.utcnow() + ): raise InvalidCacheError else: - return entry['resource'] + return entry["resource"] except (KeyError, InvalidCacheError): # (src, headers) = urllib.urlretrieve(url) try: - req = urllib.request.Request(url, None, {'User-Agent': 'Mozilla/5.0'}) + req = urllib.request.Request(url, None, {"User-Agent": "Mozilla/5.0"}) response = urllib.request.urlopen(req) except (socket.timeout, urllib.request.URLError) as e: e.args = (str(e), url) @@ -88,7 +94,10 @@ def get(self, url, expiry_callback, resource_callback=None): expiry = expiry_callback(tmp.name) if resource_callback: resource_callback(tmp.name) - self._cache[url] = {'resource': tmp.name, 'expiry': expiry.strftime(self.TIME_FORMAT)} + self._cache[url] = { + "resource": tmp.name, + "expiry": expiry.strftime(self.TIME_FORMAT), + } return tmp.name diff --git a/weather.metoffice/src/metoffice/utilities.py b/weather.metoffice/src/metoffice/utilities.py index b8bebbbae5..28e15f689b 100644 --- a/weather.metoffice/src/metoffice/utilities.py +++ b/weather.metoffice/src/metoffice/utilities.py @@ -1,17 +1,24 @@ -from functools import wraps -from datetime import datetime +import math import time import traceback -import math +from datetime import datetime +from functools import wraps + import xbmc import xbmcgui -from .constants import WEATHER_WINDOW_ID, ADDON_BROWSER_WINDOW_ID, DIALOG, TEMPERATUREUNITS, ADDON +from .constants import ( + ADDON_BROWSER_WINDOW_ID, + TEMPERATUREUNITS, + WEATHER_WINDOW_ID, + addon, + dialog, +) def log(msg, level=xbmc.LOGINFO): # by importing utilities all messages in xbmc log will be prepended with LOGPREFIX - xbmc.log('weather.metoffice: {0}'.format(msg), level) + xbmc.log("weather.metoffice: {0}".format(msg), level) def strptime(dt, fmt): @@ -26,6 +33,7 @@ def failgracefully(f): generic exception, log it and if the user is on a weather page, show something to the user in a dialog box. """ + @wraps(f) def wrapper(*args, **kwds): try: @@ -33,26 +41,33 @@ def wrapper(*args, **kwds): except Exception as e: e.args = map(str, e.args) log(traceback.format_exc(), xbmc.LOGERROR) - if len(e.args) == 0 or e.args[0] == '': - e.args = ('Error',) + if len(e.args) == 0 or e.args[0] == "": + e.args = ("Error",) if len(e.args) == 1: - e.args = e.args + ('See log file for details',) - if (xbmcgui.getCurrentWindowId() == WEATHER_WINDOW_ID or - xbmcgui.getCurrentWindowId() == ADDON_BROWSER_WINDOW_ID): + e.args = e.args + ("See log file for details",) + if ( + xbmcgui.getCurrentWindowId() == WEATHER_WINDOW_ID + or xbmcgui.getCurrentWindowId() == ADDON_BROWSER_WINDOW_ID + ): args = (e.args[0].title(),) + e.args[1:4] - DIALOG.ok(*args) # @UndefinedVariable + dialog().ok(*args) + return wrapper def xbmcbusy(f): @wraps(f) def wrapper(*args, **kwds): - if xbmcgui.getCurrentWindowId() == WEATHER_WINDOW_ID or xbmcgui.getCurrentWindowId() == ADDON_BROWSER_WINDOW_ID: + if ( + xbmcgui.getCurrentWindowId() == WEATHER_WINDOW_ID + or xbmcgui.getCurrentWindowId() == ADDON_BROWSER_WINDOW_ID + ): xbmc.executebuiltin("ActivateWindow(busydialognocancel)") try: return f(*args, **kwds) finally: xbmc.executebuiltin("Dialog.Close(busydialognocancel)") + return wrapper @@ -62,7 +77,8 @@ def wrapper(*args, **kwds): try: return f(*args, **kwds) except KeyError: - return 'n/a' + return "n/a" + return wrapper @@ -72,7 +88,8 @@ def wrapper(*args, **kwds): try: return f(*args, **kwds) except KeyError: - return 'na' + return "na" + return wrapper @@ -81,7 +98,7 @@ def minutes_as_time(minutes): Takes an integer number of minutes and returns it as a time, starting at midnight. """ - return time.strftime('%H:%M', time.gmtime(minutes*60)) + return time.strftime("%H:%M", time.gmtime(minutes * 60)) def haversine_distance(lat1, lon1, lat2, lon2): @@ -92,20 +109,21 @@ def haversine_distance(lat1, lon1, lat2, lon2): """ EARTH_RADIUS = 6371 lon1, lat1, lon2, lat2 = map(math.radians, [lon1, lat1, lon2, lat2]) - dlat = lat2-lat1 - dlon = lon2-lon1 - a = math.sin(dlat/2)**2 + \ - math.cos(lat1) * math.cos(lat2) * \ - math.sin(dlon/2)**2 + dlat = lat2 - lat1 + dlon = lon2 - lon1 + a = ( + math.sin(dlat / 2) ** 2 + + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2 + ) c = 2 * math.asin(math.sqrt(a)) return EARTH_RADIUS * c def rownd(x): try: - return str(round(float(x), 0)).split('.')[0] + return str(round(float(x), 0)).split(".")[0] except ValueError: - return '' + return "" def localised_temperature(t): @@ -114,13 +132,13 @@ def localised_temperature(t): # and other crazy units. Given that this function is only used # for non-standard pages, which require a custom skin, its # unlikely that anyone will hit the problem. - if TEMPERATUREUNITS[-1] == 'C': + if TEMPERATUREUNITS[-1] == "C": return t else: try: - return str(int(float(t)*9/5+32)) + return str(int(float(t) * 9 / 5 + 32)) except ValueError: - return '' + return "" @f_or_nla @@ -138,20 +156,22 @@ def gettext(s): gettext() gets around XBMCs cryptic "Ints For Strings" translation mechanism requires the translatable table is kept up to date with the contents of strings.po """ - translatable = {"Observation Location": 32000, - "Forecast Location": 32001, - "Regional Location": 32002, - "API Key": 32003, - "Use IP address to determine location": 32004, - "GeoIP Provider": 32005, - "Erase Cache": 32006, - "No API Key.": 32007, - "Enter your Met Office API Key under settings.": 32008, - "No Matches": 32009, - "No locations found containing": 32010, - "Matching Sites": 32011} + translatable = { + "Observation Location": 32000, + "Forecast Location": 32001, + "Regional Location": 32002, + "API Key": 32003, + "Use IP address to determine location": 32004, + "GeoIP Provider": 32005, + "Erase Cache": 32006, + "No API Key.": 32007, + "Enter your Met Office API Key under settings.": 32008, + "No Matches": 32009, + "No locations found containing": 32010, + "Matching Sites": 32011, + } try: - translation = ADDON.getLocalizedString(translatable[s]) + translation = addon().getLocalizedString(translatable[s]) if not translation: raise TranslationError else: diff --git a/weather.metoffice/src/setlocation.py b/weather.metoffice/src/setlocation.py index fc2aea7e93..9e65a51eaa 100644 --- a/weather.metoffice/src/setlocation.py +++ b/weather.metoffice/src/setlocation.py @@ -5,30 +5,54 @@ the user. On successful selection internal addon setting is set. """ +import json from datetime import datetime, timedelta from operator import itemgetter -import json -from metoffice import utilities, urlcache -from metoffice.utilities import gettext as _ +from urllib.error import HTTPError +import xbmc + +from metoffice import urlcache, utilities from metoffice.constants import ( - ADDON_DATA_PATH, GEOIP_PROVIDER, KEYBOARD, DIALOG, - ADDON, FORECAST_SITELIST_URL, OBSERVATION_SITELIST_URL, - REGIONAL_SITELIST_URL, LONG_REGIONAL_NAMES, GEOLOCATION + ADDON_DATA_PATH, + FORECAST_SITELIST_URL, + GEOIP_PROVIDER, + GEOLOCATION, + LONG_REGIONAL_NAMES, + OBSERVATION_SITELIST_URL, + REGIONAL_SITELIST_URL, + addon, + dialog, ) +from metoffice.utilities import gettext as _ @utilities.xbmcbusy def getsitelist(location, text=""): with urlcache.URLCache(ADDON_DATA_PATH) as cache: - url = {'ForecastLocation': FORECAST_SITELIST_URL, - 'ObservationLocation': OBSERVATION_SITELIST_URL, - 'RegionalLocation': REGIONAL_SITELIST_URL}[location] - filename = cache.get(url, lambda x: datetime.now()+timedelta(weeks=1)) - with open(filename, encoding='utf-8') as fh: + url = { + "ForecastLocation": FORECAST_SITELIST_URL, + "ObservationLocation": OBSERVATION_SITELIST_URL, + "RegionalLocation": REGIONAL_SITELIST_URL, + }[location] + utilities.log("Fetching %s site list from the Met Office..." % location) + try: + filename = cache.get(url, lambda x: datetime.now() + timedelta(weeks=1)) + except HTTPError: + dialog().ok( + _("Error fetching %s site list" % location), + _("Check your Met Office API Key under settings and try again."), + ) + utilities.log( + "Error fetching %s site list. Check your API Key and try again" + % location, + xbmc.LOGERROR, + ) + raise + with open(filename, encoding="utf-8") as fh: data = json.load(fh) - sitelist = data['Locations']['Location'] - if location == 'RegionalLocation': + sitelist = data["Locations"]["Location"] + if location == "RegionalLocation": # fix datapoint bug where keys start with @ in Regional Sitelist # Fixing up keys has to be a two step process. If we pop and add # in the same loop we'll get `RuntimeError: dictionary keys @@ -37,69 +61,90 @@ def getsitelist(location, text=""): # First add the correct keys. toremove = [] for key in site: - if key.startswith('@'): + if key.startswith("@"): toremove.append(key) # Now remove the keys we found above. for key in toremove: site[key[1:]] = site.pop(key) # Change regional names to long versions. Untouched otherwise. - site['name'] = LONG_REGIONAL_NAMES.get(site['name'], site['name']) + site["name"] = LONG_REGIONAL_NAMES.get(site["name"], site["name"]) if text: - sitelist[:] = filter(lambda x: x['name'].lower().find(text.lower()) >= 0, sitelist) + sitelist[:] = filter( + lambda x: x["name"].lower().find(text.lower()) >= 0, sitelist + ) - if GEOLOCATION == 'true': + if GEOLOCATION == "true": geo = {} - url = GEOIP_PROVIDER['url'] - filename = cache.get(url, lambda x: datetime.now()+timedelta(hours=1)) + url = GEOIP_PROVIDER["url"] + filename = cache.get(url, lambda x: datetime.now() + timedelta(hours=1)) try: with open(filename) as fh: data = json.load(fh) except ValueError: - utilities.log('Failed to fetch valid data from %s' % url) + utilities.log("Failed to fetch valid data from %s" % url) try: - geolat = float(data[GEOIP_PROVIDER['latitude']]) - geolong = float(data[GEOIP_PROVIDER['longitude']]) - geo = {'lat': geolat, 'long': geolong} + geolat = float(data[GEOIP_PROVIDER["latitude"]]) + geolong = float(data[GEOIP_PROVIDER["longitude"]]) + geo = {"lat": geolat, "long": geolong} except KeyError: - utilities.log('Couldn\'t extract lat/long data from %s' % url) + utilities.log("Couldn't extract lat/long data from %s" % url) for site in sitelist: try: - site['distance'] = int(utilities.haversine_distance(geo['lat'], geo['long'], - float(site['latitude']), float(site['longitude']))) - site['display'] = "{0} ({1}km)".format(site['name'], site['distance']) + site["distance"] = int( + utilities.haversine_distance( + geo["lat"], + geo["long"], + float(site["latitude"]), + float(site["longitude"]), + ) + ) + site["display"] = "{0} ({1}km)".format( + site["name"], site["distance"] + ) except KeyError: - site['display'] = site['name'] + site["display"] = site["name"] try: - sitelist = sorted(sitelist, key=itemgetter('distance')) + sitelist = sorted(sitelist, key=itemgetter("distance")) except KeyError: - sitelist = sorted(sitelist, key=itemgetter('name')) + sitelist = sorted(sitelist, key=itemgetter("name")) else: for site in sitelist: - site['display'] = site['name'] - sitelist = sorted(sitelist, key=itemgetter('name')) + site["display"] = site["name"] + sitelist = sorted(sitelist, key=itemgetter("name")) return sitelist -@utilities.failgracefully def main(location): + # In this case we _have_ to create a keyboard object so that + # we can test isConfirmed and getText. + keyboard = xbmc.Keyboard() + keyboard.doModal() + text = keyboard.isConfirmed() and keyboard.getText() - KEYBOARD.doModal() - text = KEYBOARD.isConfirmed() and KEYBOARD.getText() sitelist = getsitelist(location, text) if sitelist == []: - DIALOG.ok(_("No Matches"), _("No locations found containing")+" {0}".format(text)) + dialog().ok( + _("No Matches"), _("No locations found containing") + " {0}".format(text) + ) utilities.log("No locations found containing '%s'" % text) else: - display_list = [site['display'] for site in sitelist] - selected = DIALOG.select(_("Matching Sites"), display_list) + display_list = [site["display"] for site in sitelist] + selected = dialog().select(_("Matching Sites"), display_list) if selected != -1: - ADDON.setSetting(location, sitelist[selected]['name']) - ADDON.setSetting("%sID" % location, sitelist[selected]['id']) - ADDON.setSetting("%sLatitude" % location, str(sitelist[selected].get('latitude'))) - ADDON.setSetting("%sLongitude" % location, str(sitelist[selected].get('longitude'))) - utilities.log("Setting '{location}' to '{name} ({id})'".format(location=location, - name=sitelist[selected]['name'].encode( - 'utf-8'), - id=sitelist[selected]['id'])) + addon().setSetting(location, sitelist[selected]["name"]) + addon().setSetting("%sID" % location, sitelist[selected]["id"]) + addon().setSetting( + "%sLatitude" % location, str(sitelist[selected].get("latitude")) + ) + addon().setSetting( + "%sLongitude" % location, str(sitelist[selected].get("longitude")) + ) + utilities.log( + "Setting '{location}' to '{name} ({id})'".format( + location=location, + name=sitelist[selected]["name"].encode("utf-8"), + id=sitelist[selected]["id"], + ) + )