diff --git a/weather.ozweather/addon.xml b/weather.ozweather/addon.xml index 244021a74c..1ce38ec826 100644 --- a/weather.ozweather/addon.xml +++ b/weather.ozweather/addon.xml @@ -1,11 +1,11 @@ - + - + @@ -23,8 +23,9 @@ icon.png fanart.jpg - v2.1.5 Minor fixes -- Fix feels like calculation when data not returned by BOM, and Current.Wind + v2.1.6 Performance improvements, prep for Piers +- Improve performance when used with multiple Kodi Profiles by using a shared cache (stored under special://temp/ozweather) +- A bunch of code improvements and defensive programming to cope with scenarios where things are not at times available diff --git a/weather.ozweather/changelog.txt b/weather.ozweather/changelog.txt index 1708988033..8592049e37 100644 --- a/weather.ozweather/changelog.txt +++ b/weather.ozweather/changelog.txt @@ -1,3 +1,8 @@ +v2.1.6 Performance improvements, prep for Piers +- Improve performance when used with multiple Kodi Profiles by using a shared cache (stored under special://temp/ozweather) +- A bunch of code improvements and defensive programming to cope with scenarios where things are not at times available + + v2.1.5 Minor fixes - Fix feels like calculation when data not returned by BOM, and Current.Wind diff --git a/weather.ozweather/resources/language/resource.language.en_gb/strings.po b/weather.ozweather/resources/language/resource.language.en_gb/strings.po index f5cc77e309..c2ef8d554d 100644 --- a/weather.ozweather/resources/language/resource.language.en_gb/strings.po +++ b/weather.ozweather/resources/language/resource.language.en_gb/strings.po @@ -74,20 +74,20 @@ msgstr "" msgctxt "#32208" msgid "Enter postcode, suburb or area." -msgstr " +msgstr "" msgctxt "#32209" msgid "Radar backgrounds not showing? Try forcing a refresh" -msgstr " +msgstr "" msgctxt "#32210" msgid "Refresh all radar backgrounds on next weather fetch?" -msgstr " +msgstr "" msgctxt "#32211" msgid "Closest Radar " -msgstr " +msgstr "" msgctxt "#32212" msgid "If entered, these radar codes overrule the closest found." -msgstr " \ No newline at end of file +msgstr "" \ No newline at end of file diff --git a/weather.ozweather/resources/lib/abc/abc_video.py b/weather.ozweather/resources/lib/abc/abc_video.py index 015338cd4a..27b7c791c0 100644 --- a/weather.ozweather/resources/lib/abc/abc_video.py +++ b/weather.ozweather/resources/lib/abc/abc_video.py @@ -1,10 +1,11 @@ -# -*- coding: utf-8 -*- import requests import sys import xbmc +import xbmcgui +import json from bs4 import BeautifulSoup -# Allow for unit testing this file +# Allow for unit testing this file (remember to install kodistubs!) # This brings this addon's resources, and bossanova808 module stuff into scope # (when running this module outside Kodi) if not xbmc.getUserAgent(): @@ -12,7 +13,8 @@ sys.path.insert(0, '../../../../script.module.bossanova808/resources/lib') from resources.lib.store import Store -from bossanova808.utilities import * +from bossanova808.constants import CWD +from bossanova808.logger import Logger def scrape_and_play_abc_weather_video(): @@ -21,8 +23,11 @@ def scrape_and_play_abc_weather_video(): """ url = get_abc_weather_video_link() # Construct an offscreen list item with metadata... + if not url: + xbmcgui.Dialog().notification("OzWeather", "Couldn't retrieve ABC weather video - sorry!", xbmcgui.NOTIFICATION_ERROR, 4000) + return item = xbmcgui.ListItem(path=url) - item.setProperty('mimetype', 'video/mpeg') + item.setProperty('mimetype', 'video/mp4') item.setInfo('Video', {'title': 'ABC Weather In 90 Seconds'}) item.setArt({'thumb': f'{CWD}/resources/weather-in-90-seconds.png'}) # ...and then play it, fullscreen @@ -33,9 +38,13 @@ def scrape_and_play_abc_weather_video(): # See bottom of this file for notes on matching the video links (& Store.py for the regex) def get_abc_weather_video_link(): try: - r = requests.get(Store.ABC_URL) + r = requests.get(Store.ABC_URL, timeout=15) bs = BeautifulSoup(r.text, "html.parser") json_string = bs.find("script", {'type': 'application/json', "id": "__NEXT_DATA__"}) + if not json_string or not json_string.string: + error_msg = "ABC __NEXT_DATA__ script not found on page, couldn't extract ABC weather video link" + Logger.error(error_msg) + raise ValueError(error_msg) json_object = json.loads(json_string.string) # Logger.debug(json_object) # Put the json blob into: https://jsonhero.io/j/JU0I9LB4AlLU @@ -48,7 +57,7 @@ def get_abc_weather_video_link(): return sorted(urls, key=lambda x: x['bitrate'], reverse=True)[0]['file'] except Exception as inst: - Logger.error("Couldn't get ABC video URL from scraped page: " + str(inst)) + Logger.error(f"Couldn't get ABC video URL from scraped page: {inst}") return "" diff --git a/weather.ozweather/resources/lib/bom/bom_forecast.py b/weather.ozweather/resources/lib/bom/bom_forecast.py index 91c6c3cd73..0f38e24bdf 100644 --- a/weather.ozweather/resources/lib/bom/bom_forecast.py +++ b/weather.ozweather/resources/lib/bom/bom_forecast.py @@ -6,7 +6,7 @@ import math import xbmc -# Allow for unit testing this file +# Allow for unit testing this file e (remember to install kodistubs!) # This brings this addon's resources, and bossanova808 module stuff into scope # (when running this module outside Kodi) if not xbmc.getUserAgent(): @@ -14,7 +14,6 @@ sys.path.insert(0, '../../../../script.module.bossanova808/resources/lib') from resources.lib.store import Store -from bossanova808.utilities import * from bossanova808.logger import Logger """ @@ -48,11 +47,7 @@ def set_key(weather_data, index, key, value): if index == 0: weather_data['Current.' + key] = value.strip() - weather_data['Current.' + key] = value.strip() - weather_data['Day' + str(index) + '.' + key] = value.strip() - weather_data['Day' + str(index) + '.' + key] = value.strip() - weather_data['Daily.' + str(index + 1) + '.' + key] = value.strip() weather_data['Daily.' + str(index + 1) + '.' + key] = value.strip() @@ -121,9 +116,9 @@ def bom_forecast(geohash): bom_api_current_observations_url = f'{bom_api_url_areahash}/observations' bom_api_forecast_seven_days_url = f'{bom_api_url_areahash}/forecasts/daily' - # FUTURE? - these API end points exist, but are not yet used by OzWeather - # bom_api_forecast_three_hourly_url = f'{bom_api_url_areahash}/forecasts/3-hourly' - # bom_api_forecast_rain = f'{bom_api_url_areahash}/forecast/rain' + # FUTURE? - these API end points exist, but are not yet actually used by OzWeather + # bom_api_forecast_three_hourly_url = f"{bom_api_url_areahash}/forecasts/3-hourly" + # bom_api_forecast_rain = f"{bom_api_url_areahash}/forecast/rain" # Holders for the BOM JSON API results... area_information = None @@ -139,7 +134,7 @@ def bom_forecast(geohash): now = datetime.datetime.now() try: - r = requests.get(bom_api_area_information_url) + r = requests.get(bom_api_area_information_url, timeout=15) area_information = r.json()["data"] Logger.debug(area_information) if area_information: @@ -153,7 +148,7 @@ def bom_forecast(geohash): # Get CURRENT OBSERVATIONS try: - r = requests.get(bom_api_current_observations_url) + r = requests.get(bom_api_current_observations_url, timeout=15) current_observations = r.json()["data"] weather_data['ObservationsUpdated'] = utc_str_to_local_str(r.json()["metadata"]["issue_time"], time_zone=location_timezone) weather_data['ObservationsStation'] = r.json()["data"]['station']['name'] @@ -173,7 +168,7 @@ def bom_forecast(geohash): # Get 7-DAY FORECAST try: - r = requests.get(bom_api_forecast_seven_days_url) + r = requests.get(bom_api_forecast_seven_days_url, timeout=15) forecast_seven_days = r.json()["data"] weather_data['ForecastUpdated'] = utc_str_to_local_str(r.json()["metadata"]["issue_time"], time_zone=location_timezone) weather_data['ForecastRegion'] = r.json()["metadata"]["forecast_region"].title() @@ -187,7 +182,7 @@ def bom_forecast(geohash): # FUTURE? # # Get 3 HOURLY FORECAST # try: - # r = requests.get(bom_api_forecast_three_hourly_url) + # r = requests.get(bom_api_forecast_three_hourly_url, timeout=15) # forecast_three_hourly = r.json()["data"] # log(forecast_three_hourly) # @@ -197,7 +192,7 @@ def bom_forecast(geohash): # # # Get RAIN FORECAST # try: - # r = requests.get(bom_api_forecast_rain) + # r = requests.get(bom_api_forecast_rain, timeout=15) # forecast_rain = r.json()["data"] # log(forecast_rain) # @@ -285,7 +280,7 @@ def bom_forecast(geohash): weather_data['Current.WarningsText'] = warnings_text - # 7 DAY FORECAST + # 7-DAY FORECAST if forecast_seven_days: weather_data['Current.Condition'] = forecast_seven_days[0]['short_text'] weather_data['Current.ConditionLong'] = forecast_seven_days[0]['extended_text'] diff --git a/weather.ozweather/resources/lib/bom/bom_location.py b/weather.ozweather/resources/lib/bom/bom_location.py index ac1f422d48..bd4949c8c0 100644 --- a/weather.ozweather/resources/lib/bom/bom_location.py +++ b/weather.ozweather/resources/lib/bom/bom_location.py @@ -1,16 +1,18 @@ import requests import sys import xbmc +import xbmcgui -# Allow for unit testing this file +# Allow for unit testing this file (remember to install kodistubs!) # This brings this addon's resources, and bossanova808 module stuff into scope -# (when running this module outside Kodi) +# (only when running this module *outside* of Kodi) if not xbmc.getUserAgent(): sys.path.insert(0, '../../..') sys.path.insert(0, '../../../../script.module.bossanova808/resources/lib') -from resources.lib.bom.bom_radar import * -from bossanova808.utilities import * +from resources.lib.store import Store +from resources.lib.bom.bom_radar import closest_radar_to_lat_lon +from bossanova808.constants import ADDON, ADDON_NAME, TRANSLATE from bossanova808.logger import Logger @@ -23,7 +25,7 @@ def get_bom_locations_for(text): location_geohashes = [] try: - r = requests.get(Store.BOM_API_LOCATIONS_URL, params={'search': text}) + r = requests.get(Store.BOM_API_LOCATIONS_URL, params={'search': text}, timeout=15) for result in r.json()['data']: Logger.debug(result) locations.append(f'{result["name"]}, {result["state"]} {result["postcode"]} ({result["geohash"]})') @@ -45,7 +47,7 @@ def find_bom_location(): What we need is actually a geohash we can then use with the BOM API Save the chosen result, e.g. Ascot Vale, VIC 3032 and geohash r1r11df """ - keyboard = xbmc.Keyboard('', LANGUAGE(32195), False) + keyboard = xbmc.Keyboard('', TRANSLATE(32195), False) keyboard.doModal() if keyboard.isConfirmed() and keyboard.getText() != '': @@ -68,10 +70,10 @@ def find_bom_location(): # Don't save the settings is this goes wrong location_info_url = f'https://api.weather.bom.gov.au/v1/locations/{location_geohashes[selected]}' try: - location_info = requests.get(location_info_url).json()['data'] + location_info = requests.get(location_info_url, timeout=15).json()['data'] Logger.debug(location_info) except: - Logger.debug("Error retrieving location info for geohash {location_geohashes[selected]}") + Logger.debug(f"Error retrieving location info for geohash {location_geohashes[selected]}") raise # Save the geohash and latitude and longitude of the location diff --git a/weather.ozweather/resources/lib/bom/bom_radar.py b/weather.ozweather/resources/lib/bom/bom_radar.py index ba5b243809..73946c30c3 100644 --- a/weather.ozweather/resources/lib/bom/bom_radar.py +++ b/weather.ozweather/resources/lib/bom/bom_radar.py @@ -5,24 +5,52 @@ import sys import os import time +import socket import urllib.error import urllib.parse import urllib.request from math import sin, cos, sqrt, atan2, radians import xbmc +import xbmcvfs -# Allow for unit testing this file +# Allow for unit testing this file (remember to install kodistubs!) # This brings this addon's resources, and bossanova808 module stuff into scope # (when running this module outside Kodi) if not xbmc.getUserAgent(): sys.path.insert(0, '../../..') sys.path.insert(0, '../../../../script.module.bossanova808/resources/lib') -from bossanova808.utilities import * +from bossanova808.constants import CWD from bossanova808.logger import Logger from resources.lib.store import Store +def _download_to_path(url, dst_path, timeout=15): + """ + Download from URL to destination path atomically with proper cleanup. + + :param url: URL to download from + :param dst_path: Destination file path + :param timeout: Request timeout in seconds + :raises: urllib.error.URLError, socket.timeout, OSError on failure + """ + tmp_path = dst_path + ".tmp" + try: + with urllib.request.urlopen(url, timeout=timeout) as response: + data = response.read() + with open(tmp_path, "wb") as fh: + fh.write(data) + os.replace(tmp_path, dst_path) + except (urllib.error.URLError, socket.timeout, OSError): + # Clean up any partial file + try: + if os.path.exists(tmp_path): + os.remove(tmp_path) + except OSError: + pass + raise + + # noinspection PyPep8Naming def get_distance(point1, point2): """ @@ -61,19 +89,6 @@ def closest_radar_to_lat_lon(point): return closest_radar -def dump_all_radar_backgrounds(all_backgrounds_path=None): - """ - Remove the entire radar backgrounds folder, so that new ones will be pulled on next weather data refresh - """ - if all_backgrounds_path is None: - all_backgrounds_path = xbmcvfs.translatePath( - "special://profile/addon_data/weather.ozweather/radarbackgrounds/") - if os.path.isdir(all_backgrounds_path): - shutil.rmtree(all_backgrounds_path) - # Little pause to make sure this is complete before any refresh... - time.sleep(0.5) - - def download_background(radar_code, file_name, path): """ Downloads a radar background given a BOM radar code like IDR023 & an output filename @@ -95,43 +110,34 @@ def download_background(radar_code, file_name, path): # Append the radar code file_name = radar_code + "." + file_name - # No longer do this - as this trips up the BOMs filter - # New approach - if file is missing, then try get it - # If it is not missing, don't do anything....but user can purge old/stale files manually in the addon settings - - # # Delete backgrounds older than a week old - # if os.path.isfile(backgrounds_path + out_file_name) and ADDON.getSetting('BGDownloadToggle'): - # file_creation = os.path.getmtime(backgrounds_path + out_file_name) - # now = time.time() - # week_ago = now - 7 * 60 * 60 * 24 # Number of seconds in a week - # # log ("file creation: " + str(file_creation) + " week_ago " + str(week_ago)) - # if file_creation < week_ago: - # log("Backgrounds stale (> one week) - refreshing - " + out_file_name) - # os.remove(backgrounds_path + out_file_name) - # else: - # log("Using cached background - " + out_file_name) - # Download the backgrounds only if we don't have them yet - if not os.path.isfile(path + out_file_name): - - Logger.debug("Downloading missing background image....[%s] as [%s]" % (file_name, out_file_name)) - - if file_name == 'IDR00004.background.png': - # Special case: national radar background periodically seems to be missing from BOM ftp? Use a local copy... - Logger.debug("Copying local copy of national radar background") - # No need to do this if we're unit testing outside Kodi - if CWD: - shutil.copy(f"{CWD}/resources/IDR00004.background.png", f"{path}/background.png") - else: - url_to_get = Store.BOM_RADAR_BACKGROUND_FTPSTUB + file_name + if not os.path.isfile(os.path.join(path, out_file_name)): + Logger.debug(f"Downloading missing background image....[{file_name}] as [{out_file_name}]") + + # Try bundled national background first (if available) + if file_name == 'IDR00004.background.png' and CWD: + src = os.path.join(CWD, "resources", "IDR00004.background.png") + if os.path.isfile(src): + Logger.debug("Copying bundled national radar background") + try: + shutil.copy(src, os.path.join(path, "background.png")) + return + except (OSError, shutil.Error) as e: + Logger.warning(f"Local copy of national radar background failed: {e} — will attempt remote fetch") + + # Fallback: fetch from BOM for all backgrounds (FTP first, then HTTP) + ftp_url = Store.BOM_RADAR_BACKGROUND_FTPSTUB + file_name + dst = os.path.join(path, out_file_name) + try: + _download_to_path(ftp_url, dst) + except (urllib.error.URLError, socket.timeout, OSError) as e: + Logger.warning(f"FTP fetch failed for background {ftp_url}: {e} — trying HTTP") + http_url = Store.BOM_RADAR_HTTPSTUB + os.path.basename(ftp_url) try: - radar_image = urllib.request.urlopen(url_to_get) - with open(path + "/" + out_file_name, "wb") as fh: - fh.write(radar_image.read()) - - except Exception as e: - Logger.error(f"Failed to retrieve radar background image: {url_to_get}, exception: {str(e)}") + _download_to_path(http_url, dst) + except (urllib.error.URLError, socket.timeout, OSError) as e2: + Logger.error(f"Failed to retrieve radar background via FTP and HTTP: {ftp_url} | {http_url}, exception: {e2}") else: Logger.debug(f"Using cached {out_file_name}") @@ -227,42 +233,26 @@ def build_images(radar_code, path, loop_path): # first we retrieve a list of the available files via ftp Logger.debug("Download the radar loop") - files = [] - Logger.debug("Log in to BOM FTP") - attempts = 0 - ftp = None - - # Try up to 3 times, with a seconds pause between each, to connect to BOM FTP - # (to try and get past very occasional 'too many users' errors) - while not ftp and attempts < 3: - # noinspection PyBroadException + # Try up to 3 times, with an exponential pause between each, to connect to BOM FTP + # (to try and get past _very_ occasional 'too many users' errors) + last_err = None + for attempt in range(3): try: - ftp = ftplib.FTP("ftp.bom.gov.au") - except: - attempts += 1 - time.sleep(1) - - if not ftp: - Logger.error("Failed after 3 attempt to connect to BOM FTP - can't update radar loop") + with ftplib.FTP("ftp.bom.gov.au", timeout=15) as ftp: + ftp.login("anonymous", "anonymous@anonymous.org") + ftp.cwd("/anon/gen/radar/") + files = ftp.nlst(f"{radar_code}*") + files.sort(reverse=True) + break + except ftplib.all_errors as e: + last_err = e + # 0.5s, 1s, 2s + time.sleep(0.5 * (2 ** attempt)) + else: + Logger.error(f"Failed after 3 attempts to connect/list BOM FTP: {last_err}") return - ftp.login("anonymous", "anonymous@anonymous.org") - ftp.cwd("/anon/gen/radar/") - - Logger.debug("Get files list") - # connected, so let's get the list - try: - # BOM FTP still, in 2021, does not support the nicer mdst() operation - files = ftp.nlst() - files.sort(reverse=True) - except ftplib.error_perm as resp: - if str(resp) == "550 No files found": - Logger.error("No files in BOM ftp directory!") - else: - Logger.error("Something wrong in the ftp bit of radar images") - Logger.error(str(resp)) - Logger.debug("Download new files, and rename existing files, to avoid Kodi caching issues with the animated radar") # OK now we need just the matching radar files... # Maximum of 13 files (65 minutes, just over an hour, at 5 minutes each) @@ -279,7 +269,8 @@ def build_images(radar_code, path, loop_path): # which is why we can test here with the current time_now to see if we already have the images) if loop_pic_names: for f in loop_pic_names: - if not os.path.isfile(loop_path + time_now + "." + f): + candidate = os.path.join(loop_path, f"{time_now}.{f}") + if not os.path.isfile(candidate): # ignore the composite gif... if f[-3:] == "png": image_to_retrieve = Store.BOM_RADAR_FTPSTUB + f @@ -287,13 +278,14 @@ def build_images(radar_code, path, loop_path): Logger.debug("Retrieving new radar image: " + image_to_retrieve) Logger.debug("Output to file: " + output_file) + dst = os.path.join(loop_path, output_file) try: - radar_image = urllib.request.urlopen(image_to_retrieve) - with open(loop_path + "/" + output_file, "wb") as fh: - fh.write(radar_image.read()) + _download_to_path(image_to_retrieve, dst) + Logger.debug(f"Successfully downloaded radar image: {f}") + except (urllib.error.URLError, socket.timeout, OSError) as e: + Logger.error(f"Failed to retrieve radar loop image via FTP: {image_to_retrieve}, exception: {e}") + continue - except Exception as e: - Logger.error(f"Failed to retrieve radar image: {image_to_retrieve}, exception: {str(e)}") else: Logger.debug("Using cached radar image: " + time_now + "." + f) diff --git a/weather.ozweather/resources/lib/bom/bom_radar_scrape_latest.py b/weather.ozweather/resources/lib/bom/bom_radar_scrape_latest.py index 4e715f0b4b..f90a149dfa 100644 --- a/weather.ozweather/resources/lib/bom/bom_radar_scrape_latest.py +++ b/weather.ozweather/resources/lib/bom/bom_radar_scrape_latest.py @@ -11,7 +11,7 @@ # The master page for the BOM radars radar_page = "http://www.bom.gov.au/australia/radar/about/radar_site_info.shtml" # Needed to bypass the BOM's stupid web scraping filter -headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0'} +headers = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0'} r = requests.get(radar_page, headers=headers) soup = BeautifulSoup(r.text, 'html.parser') @@ -40,42 +40,72 @@ for site_info in site_infos: # Get the radar name - name = site_info.find("h2").text - name = name.strip() + name_element = site_info.find("h2") + if not name_element: + print(f"Warning: No h2 element found in site-info, skipping...") + continue + + name = name_element.text.strip() # Get the radar code like IDR023 link_item = site_info.find('li', class_='link') + if not link_item: + print(f"Warning: No li with class='link' found for {name}, skipping...") + continue + anchor = link_item.find('a') - radar_code_matches = re.search(r'IDR\d+', anchor.get('href')) - radar_code = radar_code_matches.group(0) or None + if not anchor: + print(f"Warning: No anchor found in link li for {name}, skipping...") + continue + + href_attr = anchor.get('href') + if not href_attr: + print(f"Warning: No href attribute found for {name}, skipping...") + continue + + radar_code_matches = re.search(r'IDR\d+', href_attr) + if not radar_code_matches: + print(f"Warning: No IDR code found in href for {name}, skipping...") + continue + + radar_code = radar_code_matches.group(0) # Get the latitude and longitude lat_lon_li = site_info.find('li') + if not lat_lon_li: + print(f"Warning: No li element found for coordinates for {name}, skipping...") + continue + text = lat_lon_li.text text = text.replace("\n", "") lat_lon_matches = re.findall(r'([+-]?[0-9]+\.[0-9]+)', lat_lon_li.text) + + if len(lat_lon_matches) < 2: + print(f"Warning: Could not extract lat/lon coordinates for {name}, skipping...") + continue + # bizarrely Mildura is the only one that has the '-' in the latitude, the rest all say 'south' - if lat_lon_matches[0][0] == '-': - lat_lon_matches[0] = lat_lon_matches[0][1:] + lat = lat_lon_matches[0] + if lat.startswith('-'): + lat = lat[1:] # Remove the leading minus sign + + lon = lat_lon_matches[1] - python_var += f' (-{lat_lon_matches[0]}, {lat_lon_matches[1]}, "{name}", "{radar_code}"),\n' - javascript_var += f' [-{lat_lon_matches[0]}, {lat_lon_matches[1]}, "{name}", "{radar_code}"],\n' + python_var += f' (-{lat}, {lon}, "{name}", "{radar_code}"),\n' + javascript_var += f' [-{lat}, {lon}, "{name}", "{radar_code}"],\n' # Remove the final newline for neatness -python_var = python_var[:-2] -javascript_var = javascript_var[:-2] +python_var = python_var.rstrip('\n,') +javascript_var = javascript_var.rstrip('\n,') # Finally, print the Python - print(f"\n\n# (Python) Automatically generated by bom_radar_scraper_latest.py from {radar_page}") print("BOM_RADAR_LOCATIONS = [") print(python_var) print("]\n\n") # & print the Javascript version - print(f"// (Javascript) Automatically generated by bom_radar_scraper_latest.py from {radar_page}") print("const bomRadarLocations = [") print(javascript_var) print("]\n\n") - diff --git a/weather.ozweather/resources/lib/forecast.py b/weather.ozweather/resources/lib/forecast.py index 5a019d9093..080402ba06 100644 --- a/weather.ozweather/resources/lib/forecast.py +++ b/weather.ozweather/resources/lib/forecast.py @@ -1,13 +1,22 @@ -from bossanova808.constants import * -from bossanova808.utilities import * +import os +import glob +import time +import sys +import shutil + +import xbmc +import xbmcvfs + +from bossanova808.constants import ADDON, ADDON_NAME, ADDON_VERSION, WEATHER_WINDOW, CWD +from bossanova808.utilities import set_property, clear_property from bossanova808.logger import Logger # noinspection PyPackages -from .abc.abc_video import * +from .abc.abc_video import get_abc_weather_video_link # noinspection PyPackages -from .bom.bom_radar import * +from .bom.bom_radar import build_images # noinspection PyPackages -from .bom.bom_forecast import * +from .bom.bom_forecast import bom_forecast, utc_str_to_local_str def clear_properties(): @@ -16,109 +25,109 @@ def clear_properties(): """ Logger.info("Clearing all weather window properties") try: - set_property(WEATHER_WINDOW, 'Weather.IsFetched') - set_property(WEATHER_WINDOW, 'Daily.IsFetched') - - set_property(WEATHER_WINDOW, 'WeatherProviderLogo') - set_property(WEATHER_WINDOW, 'WeatherProvider') - set_property(WEATHER_WINDOW, 'WeatherVersion') - set_property(WEATHER_WINDOW, 'Location') - set_property(WEATHER_WINDOW, 'Updated') - set_property(WEATHER_WINDOW, 'Radar') - set_property(WEATHER_WINDOW, 'RadarOldest') - set_property(WEATHER_WINDOW, 'RadarNewest') - set_property(WEATHER_WINDOW, 'Video.1') - - set_property(WEATHER_WINDOW, 'Forecast.City') - set_property(WEATHER_WINDOW, 'Forecast.Country') - set_property(WEATHER_WINDOW, 'Forecast.Latitude') - set_property(WEATHER_WINDOW, 'Forecast.Longitude') - set_property(WEATHER_WINDOW, 'Forecast.Updated') - - set_property(WEATHER_WINDOW, 'ForecastUpdated') - set_property(WEATHER_WINDOW, 'ForecastRegion') - set_property(WEATHER_WINDOW, 'ForecastType') - set_property(WEATHER_WINDOW, 'ObservationsUpdated') - - set_property(WEATHER_WINDOW, 'Current.IsFetched') - set_property(WEATHER_WINDOW, 'Current.Location') - set_property(WEATHER_WINDOW, 'Current.Condition') - set_property(WEATHER_WINDOW, 'Current.ConditionLong') - set_property(WEATHER_WINDOW, 'Current.Temperature') - set_property(WEATHER_WINDOW, 'Current.Ozw_Temperature') - set_property(WEATHER_WINDOW, 'Current.Wind') - set_property(WEATHER_WINDOW, 'Current.WindSpeed') - set_property(WEATHER_WINDOW, 'Current.Ozw_WindSpeed') - set_property(WEATHER_WINDOW, 'Current.WindDirection') - set_property(WEATHER_WINDOW, 'Current.WindDegree') - set_property(WEATHER_WINDOW, 'Current.WindGust') - set_property(WEATHER_WINDOW, 'Current.Pressure') - set_property(WEATHER_WINDOW, 'Current.FireDanger') - set_property(WEATHER_WINDOW, 'Current.FireDangerText') - set_property(WEATHER_WINDOW, 'Current.Visibility') - set_property(WEATHER_WINDOW, 'Current.Humidity') - set_property(WEATHER_WINDOW, 'Current.Ozw_Humidity') - set_property(WEATHER_WINDOW, 'Current.FeelsLike') - set_property(WEATHER_WINDOW, 'Current.Ozw_FeelsLike') - set_property(WEATHER_WINDOW, 'Current.DewPoint') - set_property(WEATHER_WINDOW, 'Current.UVIndex') - set_property(WEATHER_WINDOW, 'Current.OutlookIcon') - set_property(WEATHER_WINDOW, 'Current.ConditionIcon') - set_property(WEATHER_WINDOW, 'Current.FanartCode') - set_property(WEATHER_WINDOW, 'Current.Sunrise') - set_property(WEATHER_WINDOW, 'Current.Sunset') - set_property(WEATHER_WINDOW, 'Current.RainSince9') - set_property(WEATHER_WINDOW, 'Current.RainLastHr') - set_property(WEATHER_WINDOW, 'Current.Precipitation') - set_property(WEATHER_WINDOW, 'Current.ChancePrecipitation') - set_property(WEATHER_WINDOW, 'Current.SolarRadiation') - set_property(WEATHER_WINDOW, 'Current.NowLabel') - set_property(WEATHER_WINDOW, 'Current.NowValue') - set_property(WEATHER_WINDOW, 'Current.LaterLabel') - set_property(WEATHER_WINDOW, 'Current.LaterValue') - - set_property(WEATHER_WINDOW, 'Today.IsFetched') - set_property(WEATHER_WINDOW, 'Today.Sunrise') - set_property(WEATHER_WINDOW, 'Today.Sunset') - set_property(WEATHER_WINDOW, 'Today.moonphase') - set_property(WEATHER_WINDOW, 'Today.Moonphase') + clear_property(WEATHER_WINDOW, 'Weather.IsFetched') + clear_property(WEATHER_WINDOW, 'Daily.IsFetched') + + clear_property(WEATHER_WINDOW, 'WeatherProviderLogo') + clear_property(WEATHER_WINDOW, 'WeatherProvider') + clear_property(WEATHER_WINDOW, 'WeatherVersion') + clear_property(WEATHER_WINDOW, 'Location') + clear_property(WEATHER_WINDOW, 'Updated') + clear_property(WEATHER_WINDOW, 'Radar') + clear_property(WEATHER_WINDOW, 'RadarOldest') + clear_property(WEATHER_WINDOW, 'RadarNewest') + clear_property(WEATHER_WINDOW, 'Video.1') + + clear_property(WEATHER_WINDOW, 'Forecast.City') + clear_property(WEATHER_WINDOW, 'Forecast.Country') + clear_property(WEATHER_WINDOW, 'Forecast.Latitude') + clear_property(WEATHER_WINDOW, 'Forecast.Longitude') + clear_property(WEATHER_WINDOW, 'Forecast.Updated') + + clear_property(WEATHER_WINDOW, 'ForecastUpdated') + clear_property(WEATHER_WINDOW, 'ForecastRegion') + clear_property(WEATHER_WINDOW, 'ForecastType') + clear_property(WEATHER_WINDOW, 'ObservationsUpdated') + + clear_property(WEATHER_WINDOW, 'Current.IsFetched') + clear_property(WEATHER_WINDOW, 'Current.Location') + clear_property(WEATHER_WINDOW, 'Current.Condition') + clear_property(WEATHER_WINDOW, 'Current.ConditionLong') + clear_property(WEATHER_WINDOW, 'Current.Temperature') + clear_property(WEATHER_WINDOW, 'Current.Ozw_Temperature') + clear_property(WEATHER_WINDOW, 'Current.Wind') + clear_property(WEATHER_WINDOW, 'Current.WindSpeed') + clear_property(WEATHER_WINDOW, 'Current.Ozw_WindSpeed') + clear_property(WEATHER_WINDOW, 'Current.WindDirection') + clear_property(WEATHER_WINDOW, 'Current.WindDegree') + clear_property(WEATHER_WINDOW, 'Current.WindGust') + clear_property(WEATHER_WINDOW, 'Current.Pressure') + clear_property(WEATHER_WINDOW, 'Current.FireDanger') + clear_property(WEATHER_WINDOW, 'Current.FireDangerText') + clear_property(WEATHER_WINDOW, 'Current.Visibility') + clear_property(WEATHER_WINDOW, 'Current.Humidity') + clear_property(WEATHER_WINDOW, 'Current.Ozw_Humidity') + clear_property(WEATHER_WINDOW, 'Current.FeelsLike') + clear_property(WEATHER_WINDOW, 'Current.Ozw_FeelsLike') + clear_property(WEATHER_WINDOW, 'Current.DewPoint') + clear_property(WEATHER_WINDOW, 'Current.UVIndex') + clear_property(WEATHER_WINDOW, 'Current.OutlookIcon') + clear_property(WEATHER_WINDOW, 'Current.ConditionIcon') + clear_property(WEATHER_WINDOW, 'Current.FanartCode') + clear_property(WEATHER_WINDOW, 'Current.Sunrise') + clear_property(WEATHER_WINDOW, 'Current.Sunset') + clear_property(WEATHER_WINDOW, 'Current.RainSince9') + clear_property(WEATHER_WINDOW, 'Current.RainLastHr') + clear_property(WEATHER_WINDOW, 'Current.Precipitation') + clear_property(WEATHER_WINDOW, 'Current.ChancePrecipitation') + clear_property(WEATHER_WINDOW, 'Current.SolarRadiation') + clear_property(WEATHER_WINDOW, 'Current.NowLabel') + clear_property(WEATHER_WINDOW, 'Current.NowValue') + clear_property(WEATHER_WINDOW, 'Current.LaterLabel') + clear_property(WEATHER_WINDOW, 'Current.LaterValue') + + clear_property(WEATHER_WINDOW, 'Today.IsFetched') + clear_property(WEATHER_WINDOW, 'Today.Sunrise') + clear_property(WEATHER_WINDOW, 'Today.Sunset') + clear_property(WEATHER_WINDOW, 'Today.moonphase') + clear_property(WEATHER_WINDOW, 'Today.Moonphase') # and all the properties for the forecast for count in range(0, 8): - set_property(WEATHER_WINDOW, 'Day%i.Title' % count) - set_property(WEATHER_WINDOW, 'Day%i.RainChance' % count) - set_property(WEATHER_WINDOW, 'Day%i.RainChanceAmount' % count) - set_property(WEATHER_WINDOW, 'Day%i.ChancePrecipitation' % count) - set_property(WEATHER_WINDOW, 'Day%i.Precipitation' % count) - set_property(WEATHER_WINDOW, 'Day%i.HighTemp' % count) - set_property(WEATHER_WINDOW, 'Day%i.LowTemp' % count) - set_property(WEATHER_WINDOW, 'Day%i.HighTemperature' % count) - set_property(WEATHER_WINDOW, 'Day%i.LowTemperature' % count) - set_property(WEATHER_WINDOW, 'Day%i.Outlook' % count) - set_property(WEATHER_WINDOW, 'Day%i.LongOutlookDay' % count) - set_property(WEATHER_WINDOW, 'Day%i.OutlookIcon' % count) - set_property(WEATHER_WINDOW, 'Day%i.ConditionIcon' % count) - set_property(WEATHER_WINDOW, 'Day%i.FanartCode' % count) - set_property(WEATHER_WINDOW, 'Day%i.ShortDate' % count) - set_property(WEATHER_WINDOW, 'Day%i.ShortDay' % count) - - set_property(WEATHER_WINDOW, 'Daily.%i.Title' % count) - set_property(WEATHER_WINDOW, 'Daily.%i.RainChance' % count) - set_property(WEATHER_WINDOW, 'Daily.%i.RainChanceAmount' % count) - set_property(WEATHER_WINDOW, 'Daily.%i.RainAmount' % count) - set_property(WEATHER_WINDOW, 'Daily.%i.ChancePrecipitation' % count) - set_property(WEATHER_WINDOW, 'Daily.%i.Precipitation' % count) - set_property(WEATHER_WINDOW, 'Daily.%i.HighTemp' % count) - set_property(WEATHER_WINDOW, 'Daily.%i.LowTemp' % count) - set_property(WEATHER_WINDOW, 'Daily.%i.HighTemperature' % count) - set_property(WEATHER_WINDOW, 'Daily.%i.LowTemperature' % count) - set_property(WEATHER_WINDOW, 'Daily.%i.Outlook' % count) - set_property(WEATHER_WINDOW, 'Daily.%i.LongOutlookDay' % count) - set_property(WEATHER_WINDOW, 'Daily.%i.OutlookIcon' % count) - set_property(WEATHER_WINDOW, 'Daily.%i.ConditionIcon' % count) - set_property(WEATHER_WINDOW, 'Daily.%i.FanartCode' % count) - set_property(WEATHER_WINDOW, 'Daily.%i.ShortDate' % count) - set_property(WEATHER_WINDOW, 'Daily.%i.ShortDay' % count) + clear_property(WEATHER_WINDOW, 'Day%i.Title' % count) + clear_property(WEATHER_WINDOW, 'Day%i.RainChance' % count) + clear_property(WEATHER_WINDOW, 'Day%i.RainChanceAmount' % count) + clear_property(WEATHER_WINDOW, 'Day%i.ChancePrecipitation' % count) + clear_property(WEATHER_WINDOW, 'Day%i.Precipitation' % count) + clear_property(WEATHER_WINDOW, 'Day%i.HighTemp' % count) + clear_property(WEATHER_WINDOW, 'Day%i.LowTemp' % count) + clear_property(WEATHER_WINDOW, 'Day%i.HighTemperature' % count) + clear_property(WEATHER_WINDOW, 'Day%i.LowTemperature' % count) + clear_property(WEATHER_WINDOW, 'Day%i.Outlook' % count) + clear_property(WEATHER_WINDOW, 'Day%i.LongOutlookDay' % count) + clear_property(WEATHER_WINDOW, 'Day%i.OutlookIcon' % count) + clear_property(WEATHER_WINDOW, 'Day%i.ConditionIcon' % count) + clear_property(WEATHER_WINDOW, 'Day%i.FanartCode' % count) + clear_property(WEATHER_WINDOW, 'Day%i.ShortDate' % count) + clear_property(WEATHER_WINDOW, 'Day%i.ShortDay' % count) + + clear_property(WEATHER_WINDOW, 'Daily.%i.Title' % count) + clear_property(WEATHER_WINDOW, 'Daily.%i.RainChance' % count) + clear_property(WEATHER_WINDOW, 'Daily.%i.RainChanceAmount' % count) + clear_property(WEATHER_WINDOW, 'Daily.%i.RainAmount' % count) + clear_property(WEATHER_WINDOW, 'Daily.%i.ChancePrecipitation' % count) + clear_property(WEATHER_WINDOW, 'Daily.%i.Precipitation' % count) + clear_property(WEATHER_WINDOW, 'Daily.%i.HighTemp' % count) + clear_property(WEATHER_WINDOW, 'Daily.%i.LowTemp' % count) + clear_property(WEATHER_WINDOW, 'Daily.%i.HighTemperature' % count) + clear_property(WEATHER_WINDOW, 'Daily.%i.LowTemperature' % count) + clear_property(WEATHER_WINDOW, 'Daily.%i.Outlook' % count) + clear_property(WEATHER_WINDOW, 'Daily.%i.LongOutlookDay' % count) + clear_property(WEATHER_WINDOW, 'Daily.%i.OutlookIcon' % count) + clear_property(WEATHER_WINDOW, 'Daily.%i.ConditionIcon' % count) + clear_property(WEATHER_WINDOW, 'Daily.%i.FanartCode' % count) + clear_property(WEATHER_WINDOW, 'Daily.%i.ShortDate' % count) + clear_property(WEATHER_WINDOW, 'Daily.%i.ShortDay' % count) except Exception: Logger.error("********** Oz Weather Couldn't clear all the properties, sorry!!") @@ -127,30 +136,38 @@ def clear_properties(): # noinspection PyShadowingNames def forecast(geohash, radar_code): """ - The main weather data retrieval function - Does either a basic forecast, or a more extended forecast with radar etc. - :param geohash: the BOM geohash for the location - :param radar_code: the BOM radar code (e.g. 'IDR063') to retrieve the radar loop for + Retrieve forecast data from the BOM and populate Kodi weather window properties. + + Performs an optional extended update: may purge stored radar backgrounds, build radar background and loop images for the supplied radar code, set loop time labels from generated image filenames, fetch an ABC weather video link, and write all retrieved weather and status properties to the weather window (including fetch flags and update timestamp). + + Parameters: + geohash (str): BOM geohash for the location. + radar_code (str): BOM radar code (e.g. 'IDR063') used to build radar backgrounds and loop images. """ extended_features = ADDON.getSettingBool('ExtendedFeaturesToggle') Logger.debug(f'Extended features: {extended_features}') - purge_backgrounds = ADDON.getSettingBool('PurgeRadarBackgroundsOnNextRefresh') - Logger.debug(f'Purge Backgrounds: {purge_backgrounds}') - # Has the user requested we refresh the radar backgrounds on next weather fetch? - if purge_backgrounds: - Logger.info("Purging all radar backgrounds") - dump_all_radar_backgrounds() + # Has the user requested we refresh the radar data on next weather fetch? + purge_radar_backgrounds = ADDON.getSettingBool('PurgeRadarBackgroundsOnNextRefresh') + if purge_radar_backgrounds: + Logger.info("Purging all radar background per user request") + if os.path.isdir(xbmcvfs.translatePath("special://temp/ozweather/backgrounds")): + shutil.rmtree(xbmcvfs.translatePath("special://temp/ozweather/backgrounds")) + # Little pause to make sure this is complete before any weather refresh... + time.sleep(0.5) ADDON.setSetting('PurgeRadarBackgroundsOnNextRefresh', 'false') # Get the radar images first - because it looks better on refreshes if extended_features: Logger.debug(f'Getting radar images for {radar_code}') - backgrounds_path = xbmcvfs.translatePath( - "special://profile/addon_data/weather.ozweather/radarbackgrounds/" + radar_code + "/") - overlay_loop_path = xbmcvfs.translatePath( - "special://profile/addon_data/weather.ozweather/currentloop/" + radar_code + "/") + # Use cache for all radar data (backgrounds and current loop images) + # Kodi does not routinely clear this on exit (so the backgrounds are conserved as desired) + # OzWeather takes care of deleting the ephemeral (loop) images as needed + # Seems the best place, see: https://forum.kodi.tv/showthread.php?tid=382805 + # (If the cache is cleared at any point, OzWeather will then re-download what it needs). + backgrounds_path = xbmcvfs.translatePath(f"special://temp/ozweather/backgrounds/{radar_code}/") + overlay_loop_path = xbmcvfs.translatePath(f"special://temp/ozweather/loop/{radar_code}/") build_images(radar_code, backgrounds_path, overlay_loop_path) set_property(WEATHER_WINDOW, 'Radar', radar_code) @@ -216,7 +233,7 @@ def get_weather(): # This is/was an attempt to use conditions in skins to basically auto-adapt the MyWeather.xml and all OzWeather # components to the currently-in-use skin. However, no matter what I try I can't get the conditions to work - # in the skin files. + # in the skin files. This is still used by my OzWeather Skin Patcher addon, however, so left here. # noinspection PyBroadException try: skin_in_use = xbmc.getSkinDir().split('.')[1] @@ -258,7 +275,7 @@ def get_weather(): latitude = ADDON.getSetting(f'Location{sys.argv[1]}Lat') longitude = ADDON.getSetting(f'Location{sys.argv[1]}Lon') try: - location_in_use = location_in_use[0:location_in_use.index(' (')] + location_in_use = location_in_use[0:location_in_use.index(',')] except ValueError: pass @@ -269,4 +286,6 @@ def get_weather(): set_property(WEATHER_WINDOW, 'Forecast.Country', "Australia") set_property(WEATHER_WINDOW, 'Forecast.Latitude', latitude) set_property(WEATHER_WINDOW, 'Forecast.Longitude', longitude) - set_property(WEATHER_WINDOW, 'Forecast.Updated', time.strftime("%d/%m @ %H:%M").lower()) + time_updated = time.strftime("%d/%m @ %H:%M").lower() + set_property(WEATHER_WINDOW, 'Forecast.Updated', time_updated) + set_property(WEATHER_WINDOW, 'LastUpdated', time_updated) diff --git a/weather.ozweather/resources/lib/locations.py b/weather.ozweather/resources/lib/locations.py index 218a8eeed0..a0bf071ce0 100644 --- a/weather.ozweather/resources/lib/locations.py +++ b/weather.ozweather/resources/lib/locations.py @@ -1,4 +1,5 @@ -from bossanova808.utilities import * +from bossanova808.constants import ADDON, WEATHER_WINDOW +from bossanova808.utilities import set_property from bossanova808.logger import Logger @@ -12,9 +13,9 @@ def refresh_locations(): location2 = ADDON.getSetting('Location2BOM') or "" location3 = ADDON.getSetting('Location3BOM') or "" - Logger.info("Location1: " + location1) - Logger.info("Location2: " + location2) - Logger.info("Location3: " + location3) + Logger.info(f"Location1: {location1}") + Logger.info(f"Location2: {location2}") + Logger.info(f"Location3: {location3}") locations = 0 @@ -41,9 +42,9 @@ def refresh_locations(): radar2 = ADDON.getSetting('Radar2') or ADDON.getSetting('Location2ClosestRadar') or "" radar3 = ADDON.getSetting('Radar3') or ADDON.getSetting('Location3ClosestRadar') or "" - Logger.info("Radar1: " + radar1) - Logger.info("Radar2: " + radar2) - Logger.info("Radar3: " + radar3) + Logger.info(f"Radar1: {radar1}") + Logger.info(f"Radar2: {radar2}") + Logger.info(f"Radar3: {radar3}") radars = 0 diff --git a/weather.ozweather/resources/lib/ozweather.py b/weather.ozweather/resources/lib/ozweather.py index 38745359c0..cbd9a69fa4 100644 --- a/weather.ozweather/resources/lib/ozweather.py +++ b/weather.ozweather/resources/lib/ozweather.py @@ -1,16 +1,15 @@ -# -*- coding: utf-8 -*- import socket # noinspection PyPackages -from .forecast import * +from .bom.bom_location import find_bom_location # noinspection PyPackages -from .locations import * +from .abc.abc_video import scrape_and_play_abc_weather_video # noinspection PyPackages -from .bom.bom_location import * +from .forecast import get_weather # noinspection PyPackages -from .abc.abc_video import * +from .locations import refresh_locations -from bossanova808.utilities import * +from bossanova808.logger import Logger def run(args): @@ -21,27 +20,31 @@ def run(args): :param args: sys.argv is passed directly through """ - footprints() - socket.setdefaulttimeout(100) + Logger.start() + try: + socket.setdefaulttimeout(100) - # RUN MODE - ADDON CALLED FORM Kodi SETTINGS - # the addon is being called from the settings section where the user enters their postcodes - if args[1].startswith('Location'): - find_bom_location() + arg = args[1] if len(args) > 1 else "" - # RUN MODE - ADDON CALLED FORM Kodi SETTINGS - # the addon is being called from the settings section where the user enters their postcodes - elif args[1].startswith('ABC'): - scrape_and_play_abc_weather_video() + # RUN MODE - ADDON CALLED FORM Kodi SETTINGS + # the addon is being called from the settings section where the user enters their postcodes + if arg.startswith('Location'): + find_bom_location() - # RUN MODE - GET WEATHER OBSERVATIONS AND FORECAST - # script is being called in general use, not from the settings page - # sys.argv[1] has the current location number (e.g. '1'), so fetch the weather data - else: - get_weather() + # RUN MODE - ADDON CALLED FROM Kodi SETTINGS + # the addon is being called from the settings section where the user enters their postcodes + elif arg.startswith('ABC'): + scrape_and_play_abc_weather_video() - # If location settings have changed, this kick-starts an update - refresh_locations() + # RUN MODE - GET WEATHER OBSERVATIONS AND FORECAST + # script is being called in general use, not from the settings page + # sys.argv[1] has the current location number (e.g. '1'), so fetch the weather data + else: + get_weather() + + # If location settings have changed, this kick-starts an update + refresh_locations() # and close out... - footprints(startup=False) + finally: + Logger.stop() diff --git a/weather.ozweather/resources/lib/store.py b/weather.ozweather/resources/lib/store.py index 0aace10558..14d394fb67 100644 --- a/weather.ozweather/resources/lib/store.py +++ b/weather.ozweather/resources/lib/store.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# noinspection SpellCheckingInspection class Store: """ Helper class to provide a centralised store for CONSTANTS and globals @@ -24,7 +22,7 @@ class Store: # The below list is generated using my scraper: python3 resources/lib/bom/bom_radar_scrape_latest.py # Just cut and paste the output of that below.... # The master URL is: http://www.bom.gov.au/australia/radar/about/radar_site_info.shtml - # Can cross check with: https://github.com/theOzzieRat/bom-radar-card/blob/master/src/bom-radar-card.ts around line 130 + # Can cross-check with: https://github.com/theOzzieRat/bom-radar-card/blob/master/src/bom-radar-card.ts around line 130 # (Python) Automatically generated by bom_radar_scraper_latest.py from http://www.bom.gov.au/australia/radar/info/nt_info.shtml BOM_RADAR_LOCATIONS = [ @@ -49,7 +47,6 @@ class Store: (-36.03, 146.03, "Yarrawonga", "IDR493"), # http://www.bom.gov.au/australia/radar/info/qld_info.shtml (-19.88, 148.08, "Bowen", "IDR243"), - (-27.39, 153.13, "Brisbane Airport", "IDR1053"), (-27.718, 153.240, "Brisbane (Mt. Stapylton)", "IDR663"), (-16.82, 145.68, "Cairns", "IDR193"), (-23.5494, 148.2392, "Emerald (Central Highlands)", "IDR723"), @@ -64,7 +61,7 @@ class Store: (-20.75, 143.14, "Richmond", "IDR1073"), (-25.696, 149.898, "Taroom", "IDR983"), (-27.2740, 151.9930, "Toowoomba", "IDR1083"), - (-19.42, 146.55, "Townsville (Hervey Range)", "IDR733"), + (-19.42, 146.55, "Townsville", "IDR1063"), (-26.44, 147.35, "Warrego", "IDR673"), (-12.67, 141.92, "Weipa", "IDR783"), (-16.288, 149.965, "Willis Island", "IDR413"), @@ -80,6 +77,7 @@ class Store: (-25.03, 128.30, "Giles", "IDR443"), (-18.23, 127.66, "Halls Creek", "IDR393"), (-30.79, 121.45, "Kalgoorlie-Boulder", "IDR483"), + (-20.99, 116.87, "Karratha", "IDR1113"), (-22.10, 114.00, "Learmonth", "IDR293"), (-33.097, 119.009, "Newdegate", "IDR383"), (-32.39, 115.87, "Perth (Serpentine)", "IDR703"), @@ -279,4 +277,3 @@ class Store: # Just a store! def __init__(self): pass - diff --git a/weather.ozweather/resources/settings.xml b/weather.ozweather/resources/settings.xml index d7d5aefdc7..5edaeb6a3b 100644 --- a/weather.ozweather/resources/settings.xml +++ b/weather.ozweather/resources/settings.xml @@ -357,7 +357,7 @@ 0 - + true @@ -406,7 +406,7 @@ 32157 - + 0