diff --git a/cinnamon-screensaver.pot b/cinnamon-screensaver.pot index 3fef178b..2f41a6b2 100644 --- a/cinnamon-screensaver.pot +++ b/cinnamon-screensaver.pot @@ -8,61 +8,61 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-02 21:29+0000\n" +"POT-Creation-Date: 2024-11-22 04:11-0600\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=CHARSET\n" +"Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" -#: src/cinnamon-screensaver-command.py:41 +#: src/cinnamon-screensaver-command.py:43 msgid "Causes the screensaver to exit gracefully" msgstr "" -#: src/cinnamon-screensaver-command.py:43 +#: src/cinnamon-screensaver-command.py:45 msgid "Query the state of the screensaver" msgstr "" -#: src/cinnamon-screensaver-command.py:45 +#: src/cinnamon-screensaver-command.py:47 msgid "Query the length of time the screensaver has been active" msgstr "" -#: src/cinnamon-screensaver-command.py:47 +#: src/cinnamon-screensaver-command.py:49 msgid "Tells the running screensaver process to lock the screen immediately" msgstr "" -#: src/cinnamon-screensaver-command.py:49 +#: src/cinnamon-screensaver-command.py:51 msgid "Turn the screensaver on (blank the screen)" msgstr "" -#: src/cinnamon-screensaver-command.py:51 +#: src/cinnamon-screensaver-command.py:53 msgid "If the screensaver is active then deactivate it (un-blank the screen)" msgstr "" -#: src/cinnamon-screensaver-command.py:53 +#: src/cinnamon-screensaver-command.py:55 msgid "Version of this application" msgstr "" -#: src/cinnamon-screensaver-command.py:55 +#: src/cinnamon-screensaver-command.py:57 msgid "Message to be displayed in lock screen" msgstr "" -#: src/cinnamon-screensaver-command.py:105 +#: src/cinnamon-screensaver-command.py:106 msgid "The screensaver is active\n" msgstr "" -#: src/cinnamon-screensaver-command.py:107 +#: src/cinnamon-screensaver-command.py:108 msgid "The screensaver is inactive\n" msgstr "" -#: src/cinnamon-screensaver-command.py:111 +#: src/cinnamon-screensaver-command.py:112 msgid "The screensaver is not currently active.\n" msgstr "" -#: src/cinnamon-screensaver-command.py:113 +#: src/cinnamon-screensaver-command.py:114 #, python-format msgid "The screensaver has been active for %d second.\n" msgid_plural "The screensaver has been active for %d seconds.\n" @@ -80,7 +80,7 @@ msgid "" "prior to this occurring." msgstr "" -#: src/passwordEntry.py:23 src/unlock.py:216 +#: src/passwordEntry.py:23 src/unlock.py:215 msgid "Please enter your password..." msgstr "" @@ -92,52 +92,56 @@ msgstr "" msgid "Switch User" msgstr "" -#: src/unlock.py:189 +#: src/unlock.py:188 msgid "Incorrect password" msgstr "" -#: src/unlock.py:206 +#: src/unlock.py:205 msgid "Checking..." msgstr "" -#: src/unlock.py:250 +#: src/unlock.py:249 msgid "You have the Caps Lock key on." msgstr "" +#: src/weather.py:102 +msgid "in" +msgstr "" + #. This is the first line of text for the backup-locker, explaining how to switch to tty #. and run 'cinnamon-unlock-desktop' command. This appears if the screensaver crashes. -#: backup-locker/cs-backup-locker.c:255 +#: backup-locker/cs-backup-locker.c:306 msgid "Something went wrong with the screensaver." msgstr "" #. (continued) This is a subtitle -#: backup-locker/cs-backup-locker.c:265 +#: backup-locker/cs-backup-locker.c:316 msgid "We'll help you get your desktop back" msgstr "" #. (new section) Bulleted list of steps to take to unlock the desktop; -#: backup-locker/cs-backup-locker.c:276 +#: backup-locker/cs-backup-locker.c:327 #, c-format msgid "Switch to a console using ." msgstr "" #. (list continued) -#: backup-locker/cs-backup-locker.c:278 +#: backup-locker/cs-backup-locker.c:329 msgid "Log in by typing your user name followed by your password." msgstr "" #. (list continued) -#: backup-locker/cs-backup-locker.c:280 +#: backup-locker/cs-backup-locker.c:331 msgid "At the prompt, type 'cinnamon-unlock-desktop' and press Enter." msgstr "" #. (list continued) -#: backup-locker/cs-backup-locker.c:282 +#: backup-locker/cs-backup-locker.c:333 #, c-format msgid "Switch back to your unlocked desktop using ." msgstr "" #. (end section) Final words after the list of steps -#: backup-locker/cs-backup-locker.c:287 +#: backup-locker/cs-backup-locker.c:338 msgid "If you can reproduce this behavior, please file a report here:" msgstr "" diff --git a/src/meson.build b/src/meson.build index dd86840b..bb2d1881 100644 --- a/src/meson.build +++ b/src/meson.build @@ -32,6 +32,7 @@ app_py = [ 'status.py', 'unlock.py', 'volumeControl.py', + 'weather.py' ] app_css = [ diff --git a/src/stage.py b/src/stage.py index c73440c9..f3c2af57 100644 --- a/src/stage.py +++ b/src/stage.py @@ -20,6 +20,7 @@ from util import utils, trackers, settings from util.eventHandler import EventHandler from util.utils import DEBUG +from weather import WeatherWidget class Stage(Gtk.Window): """ @@ -69,6 +70,7 @@ def __init__(self, manager, away_message): self.overlay = None self.clock_widget = None self.albumart_widget = None + self.weather_widget = None self.unlock_dialog = None self.audio_panel = None self.info_panel = None @@ -291,6 +293,11 @@ def setup_delayed_components(self, data=None): except Exception as e: print("Problem setting up albumart widget: %s" % str(e)) self.albumart_widget = None + try: + self.setup_weather() + except Exception as e: + print("Problem setting up weather widget: %s" % str(e)) + self.weather_widget = None try: self.setup_status_bars() except Exception as e: @@ -324,6 +331,12 @@ def destroy_children(self): except Exception as e: print(e) + try: + if self.weather_widget is not None: + self.weather_widget.destroy() + except Exception as e: + print(e) + try: if self.info_panel is not None: self.info_panel.destroy() @@ -345,6 +358,7 @@ def destroy_children(self): self.unlock_dialog = None self.clock_widget = None self.albumart_widget = None + self.weather_widget = None self.info_panel = None self.audio_panel = None self.osk = None @@ -504,6 +518,23 @@ def setup_albumart(self): if settings.get_show_albumart(): self.albumart_widget.start_positioning() + def setup_weather(self): + """ + Construct the Weather widget and add it to the overlay, but only actually + show it if we're a) Not running a plug-in, and b) The user wants it via + preferences. + + Initially invisible, regardless - its visibility is controlled via its + own positioning timer. + """ + self.weather_widget = WeatherWidget(status.screen.get_mouse_monitor()) + self.add_child_widget(self.weather_widget) + + self.floaters.append(self.weather_widget) + + if settings.get_show_weather(): + self.weather_widget.start_positioning() + def setup_osk(self): self.osk = OnScreenKeyboard() diff --git a/src/util/geojs.py b/src/util/geojs.py new file mode 100644 index 00000000..e5458916 --- /dev/null +++ b/src/util/geojs.py @@ -0,0 +1,24 @@ +import json +from types import SimpleNamespace + +import requests + +from util.location import LocationProvider, LocationData + +URL = "https://get.geojs.io/v1/ip/geo.json" + +class GeoJSLocationProvider(LocationProvider): + """ + LocationProvider implementation for geojs.io + """ + + def __init__(self): + pass + + @staticmethod + def GetLocation() -> LocationData: + response = requests.get(URL) + + data = json.loads(response.text, object_hook=lambda d: SimpleNamespace(**d)) + + return LocationData(float(data.latitude), float(data.longitude), data.city, data.country, data.timezone, data.city) \ No newline at end of file diff --git a/src/util/location.py b/src/util/location.py new file mode 100644 index 00000000..b5204b9b --- /dev/null +++ b/src/util/location.py @@ -0,0 +1,19 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class LocationData: + lat: float + lon: float + city: Optional[str] = None + country: Optional[str] = None + timeZone: Optional[str] = None + entryText: Optional[str] = None + + +class LocationProvider(ABC): + @abstractmethod + def GetLocation(self) -> LocationData: + pass diff --git a/src/util/meson.build b/src/util/meson.build index 393cacab..d2cdeb41 100644 --- a/src/util/meson.build +++ b/src/util/meson.build @@ -3,10 +3,15 @@ app_py = [ 'eventHandler.py', 'fader.py', 'focusNavigator.py', + 'geojs.py', 'keybindings.py', + 'location.py', + 'openweathermap.py', 'settings.py', 'trackers.py', - 'utils.py' + 'utils.py', + 'weather.py', + 'weather_types.py' ] install_data(app_py, install_dir: join_paths(pkgdatadir, 'util')) diff --git a/src/util/openweathermap.py b/src/util/openweathermap.py new file mode 100644 index 00000000..243c1bc9 --- /dev/null +++ b/src/util/openweathermap.py @@ -0,0 +1,271 @@ +import json +from types import SimpleNamespace + +import requests +from gi.repository import Pango + +from util.weather_types import ( + APIUniqueField, + BuiltinIcons, + Condition, + CustomIcons, + Location, + LocationData, + WeatherData, + WeatherProvider, + Wind, +) + +OWM_URL = "https://api.openweathermap.org/data/2.5/weather" +# this is the OpenWeatherMap API key used by linux-mint/cinnamon-spices-applets/weather@mockturtl +# presumably belongs to the org? +OWM_API_KEY = "1c73f8259a86c6fd43c7163b543c8640" +OWM_SUPPORTED_LANGS = [ + "af", + "al", + "ar", + "az", + "bg", + "ca", + "cz", + "da", + "de", + "el", + "en", + "eu", + "fa", + "fi", + "fr", + "gl", + "he", + "hi", + "hr", + "hu", + "id", + "it", + "ja", + "kr", + "la", + "lt", + "mk", + "no", + "nl", + "pl", + "pt", + "pt_br", + "ro", + "ru", + "se", + "sk", + "sl", + "sp", + "es", + "sr", + "th", + "tr", + "ua", + "uk", + "vi", + "zh_cn", + "zh_tw", + "zu", +] + + +class OWMWeatherProvider(WeatherProvider): + """ + WeatherProvider implementation for OpenWeatherMap.org + """ + + def __init__(self): + self.needsApiKey = False + self.prettyName = _("OpenWeatherMap") + self.name = "OpenWeatherMap_Open" + self.maxForecastSupport = 7 + self.maxHourlyForecastSupport = 0 + self.website = "https://openweathermap.org/" + self.remainingCalls = None + self.supportHourlyPrecipChance = False + self.supportHourlyPrecipVolume = False + + def GetWeather(self, loc: LocationData): + lang = self.locale_to_owm_lang(Pango.language_get_default().to_string()) + pref = list( + map( + lambda p: self.locale_to_owm_lang(p.to_string()), + Pango.language_get_preferred(), + ) + ) + if lang not in OWM_SUPPORTED_LANGS: + for locale in pref: + if self.locale_to_owm_lang(locale) in OWM_SUPPORTED_LANGS: + lang = self.locale_to_owm_lang(locale) + break + # if we still have not found a supported language... + if lang not in OWM_SUPPORTED_LANGS: + lang = "en" + + response = requests.get( + OWM_URL, + { + "lat": loc.lat, + "lon": loc.lon, + "units": "standard", + "appid": OWM_API_KEY, + "lang": lang, + }, + ) + + # actual object structure: https://github.com/linuxmint/cinnamon-spices-applets/weather@mockturtl/src/3_8/providers/openweathermap/payload/weather.ts + data = json.loads(response.text, object_hook=lambda d: SimpleNamespace(**d)) + return self.owm_data_to_weather_data(data) + + @staticmethod + def locale_to_owm_lang(locale_string): + if locale_string is None: + return "en" + + # Dialect? support by OWM + if ( + locale_string == "zh-cn" + or locale_string == "zh-cn" + or locale_string == "pt-br" + ): + return locale_string + + lang = locale_string.split("-")[0] + # OWM uses different language code for Swedish, Czech, Korean, Latvian, Norwegian + if lang == "sv": + return "se" + elif lang == "cs": + return "cz" + elif lang == "ko": + return "kr" + elif lang == "lv": + return "la" + elif lang == "nn" or lang == "nb": + return "no" + return lang + + def owm_data_to_weather_data(self, owm_data) -> WeatherData: + """ + Returns as much of a complete WeatherData object as we can + """ + return WeatherData( + **dict( + date=owm_data.dt, + sunrise=owm_data.sys.sunrise, + sunset=owm_data.sys.sunset, + coord=owm_data.coord, + location=Location( + **dict( + city=owm_data.name, + country=owm_data.sys.country, + url="https://openweathermap.org/city/%s" % owm_data.id, + ) + ), + condition=Condition( + **dict( + main=owm_data.weather[0].main, + description=owm_data.weather[0].description, + icons=self.owm_icon_to_builtin_icons(owm_data.weather[0].icon), + customIcon=self.owm_icon_to_custom_icon( + owm_data.weather[0].icon + ), + ) + ), + wind=Wind(**dict(speed=owm_data.wind.speed, degree=owm_data.wind.deg)), + temperature=owm_data.main.temp, + pressure=owm_data.main.pressure, + humidity=owm_data.main.humidity, + dewPoint=None, + extra_field=APIUniqueField( + **dict( + type="temperature", + name=_("Feels Like"), + value=owm_data.main.feels_like, + ) + ), + ) + ) + + @staticmethod + def owm_icon_to_builtin_icons(icon) -> list[BuiltinIcons]: + # https://openweathermap.org/weather-conditions + # fallback icons are: weather-clear-night + # weather-clear weather-few-clouds-night weather-few-clouds + # weather-fog weather-overcast weather-severe-alert weather-showers + # weather-showers-scattered weather-snow weather-storm + match icon: + case "10d": + # rain day */ + return [ + "weather-rain", + "weather-showers-scattered", + "weather-freezing-rain", + ] + case "10n": + # rain night */ + return [ + "weather-rain", + "weather-showers-scattered", + "weather-freezing-rain", + ] + case "09n": + # showers night*/ + return ["weather-showers"] + case "09d": + # showers day */ + return ["weather-showers"] + case "13d": + # snow day*/ + return ["weather-snow"] + case "13n": + # snow night */ + return ["weather-snow"] + case "50d": + # mist day */ + return ["weather-fog"] + case "50n": + # mist night */ + return ["weather-fog"] + case "04d": + # broken clouds day */ + return ["weather-overcast", "weather-clouds", "weather-few-clouds"] + case "04n": + # broken clouds night */ + return [ + "weather-overcast", + "weather-clouds-night", + "weather-few-clouds-night", + ] + case "03n": + # mostly cloudy (night) */ + return ["weather-clouds-night", "weather-few-clouds-night"] + case "03d": + # mostly cloudy (day) */ + return ["weather-clouds", "weather-few-clouds", "weather-overcast"] + case "02n": + # partly cloudy (night) */ + return ["weather-few-clouds-night"] + case "02d": + # partly cloudy (day) */ + return ["weather-few-clouds"] + case "01n": + # clear (night) */ + return ["weather-clear-night"] + case "01d": + # sunny */ + return ["weather-clear"] + case "11d": + # storm day */ + return ["weather-storm"] + case "11n": + # storm night */ + return ["weather-storm"] + case _: + return ["weather-severe-alert"] + + @staticmethod + def owm_icon_to_custom_icon(icon) -> CustomIcons: + return None # TODO diff --git a/src/util/settings.py b/src/util/settings.py index 58a7c4fe..fdc3354f 100644 --- a/src/util/settings.py +++ b/src/util/settings.py @@ -30,6 +30,9 @@ SHOW_CLOCK_KEY = "show-clock" SHOW_ALBUMART = "show-album-art" +SHOW_WEATHER = "show-weather" +WEATHER_LOCATION = "weather-location" +WEATHER_UNITS = "weather-units" ALLOW_SHORTCUTS = "allow-keyboard-shortcuts" ALLOW_MEDIA_CONTROL = "allow-media-control" SHOW_INFO_PANEL = "show-info-panel" @@ -56,6 +59,7 @@ # "settings.ss_settings.get_string(settings.DEFAULT_MESSAGE_KEY)" or keeping # instances of GioSettings wherever we need them. + def _check_string(string): if string and string != "": return string @@ -133,6 +137,24 @@ def get_show_clock(): def get_show_albumart(): return ss_settings.get_boolean(SHOW_ALBUMART) +def get_show_weather(): + return ss_settings.get_boolean(SHOW_WEATHER) + +def get_weather_location(): + location_string = ss_settings.get_string(WEATHER_LOCATION) # string LAT,LON + return _check_string(location_string) + +def get_weather_units(): + units = ["metric", "imperial"] + units_string = _check_string(ss_settings.get_string(WEATHER_UNITS)) + return units_string if units_string in units else "metric" + +def get_weather_font(): + # reusing the Clock widget Time font for now (it's big) + time_font = ss_settings.get_string(FONT_TIME_KEY) + + return _check_string(time_font) + def get_allow_shortcuts(): return ss_settings.get_boolean(ALLOW_SHORTCUTS) diff --git a/src/util/weather.py b/src/util/weather.py new file mode 100644 index 00000000..e7ffd4e7 --- /dev/null +++ b/src/util/weather.py @@ -0,0 +1,6 @@ +def k_to_c(k: float) -> float: + return round(k - 273.15, 1) + + +def k_to_f(k: float) -> int: + return round((9 / 5 * (k - 273.15) + 32)) diff --git a/src/util/weather_types.py b/src/util/weather_types.py new file mode 100644 index 00000000..b3e257be --- /dev/null +++ b/src/util/weather_types.py @@ -0,0 +1,194 @@ +# Mostly derived from/compatible with: +# https://github.com/linuxmint/cinnamon-spices-applets/weather@mockturtl/src/3_8/types.ts +# we don't use much of the data, but intending for easy porting of sources + customizations going forward +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Literal, Optional + +from util.location import LocationData +from util.weather import k_to_c, k_to_f + +type PrecipitationTypes = Literal[ + "rain", "snow", "none", "ice pellets", "freezing rain" +] +type BuiltinIcons = Literal[ + "weather-clear", + "weather-clear-night", + "weather-few-clouds", + "weather-few-clouds-night", + "weather-clouds", + "weather-many-clouds", + "weather-overcast", + "weather-showers-scattered", + "weather-showers-scattered-day", + "weather-showers-scattered-night", + "weather-showers-day", + "weather-showers-night", + "weather-showers", + "weather-rain", + "weather-freezing-rain", + "weather-snow", + "weather-snow-day", + "weather-snow-night", + "weather-snow-rain", + "weather-snow-scattered", + "weather-snow-scattered-day", + "weather-snow-scattered-night", + "weather-storm", + "weather-hail", + "weather-fog", + "weather-tornado", + "weather-windy", + "weather-breeze", + "weather-clouds-night", + "weather-severe-alert", +] +type CustomIcons = Literal[None] # TODO + + +@dataclass +class Coord: + lat: float + lon: float + + +@dataclass +class Location: + city: Optional[str] = None + country: Optional[str] = None + timeZone: Optional[str] = None + url: Optional[str] = None + tzOffset: Optional[float] = None + + +@dataclass +class StationInfo: + distanceFrom: float + name: Optional[str] = None + lat: Optional[float] = None + lon: Optional[float] = None + area: Optional[str] = None + + +@dataclass +class Wind: + # m/s + speed: float + # meteorological degrees + degree: float + + +@dataclass +class Condition: + main: str + description: str + icons: Optional[list[BuiltinIcons]] = None + customIcon: Optional[CustomIcons] = None # TODO + + +class ForecastData: + date: int + temp_min: float # kelvin + temp_max: float # kelvin + condition: Condition + + +@dataclass +class Precipitation: + type: PrecipitationTypes + # /** in mm */ + volume: Optional[float] = None + # /** % */ + chance: Optional[float] = None + + +@dataclass +class HourlyForecastData: + date: int + # /** Kelvin */ + temp: float + condition: Condition + precipitation: Optional[Precipitation] = None + + +type APIUniqueFieldTypes = Literal["temperature", "percent", "string"] + + +@dataclass +class APIUniqueField: + name: str + value: str | float + type: APIUniqueFieldTypes + + +@dataclass +class ImmediatePrecipitation: + start: int + end: int + + +type AlertSeverity = Literal["minor", "moderate", "severe", "extreme", "unknown"] + + +@dataclass +class AlertData: + sender_name: str + level: AlertSeverity + title: str + description: str + icon: Optional[BuiltinIcons | CustomIcons] = None + + +@dataclass +class WeatherData: + date: int + coord: Coord + location: Location + condition: Condition + wind: Wind + stationInfo: Optional[StationInfo] = None + # /** in UTC with tz info */ + sunrise: Optional[float] = None + # /** in UTC with tz info */ + sunset: Optional[float] = None + # /** In Kelvin */ + temperature: Optional[float] = None + # /** In hPa */ + pressure: Optional[float] = None + # /** In percent */ + humidity: Optional[float] = None + # /** In kelvin */ + dewPoint: Optional[float] = None + forecasts: Optional[list[ForecastData]] = None + hourlyForecasts: Optional[list[HourlyForecastData]] = None + extra_field: Optional[APIUniqueField] = None + immediatePrecipitation: Optional[ImmediatePrecipitation] = None + alerts: Optional[list[AlertData]] = None + + def temp_f(self): + return k_to_f(self.temperature) if self.temperature else None + + def temp_c(self): + return k_to_c(self.temperature) if self.temperature else None + + +class WeatherProvider(ABC): + """ + WeatherProvider tries to emulate the interface specified in cinnamon-spices-applets/weather@mockturtl + such that other providers could be easily ported here in the future + """ + + needsApiKey: bool + prettyName: str + name: Literal["OpenWeatherMap_Open"] # expand in the future + maxForecastSupport: int + maxHourlyForecastSupport: int + website: str + remainingCalls: Optional[int] = None + supportHourlyPrecipChance: bool + supportHourlyPrecipVolume: bool + locationType: Literal["coordinates", "postcode"] + + @abstractmethod + def GetWeather(self, loc: LocationData) -> WeatherData: + pass diff --git a/src/weather.py b/src/weather.py new file mode 100644 index 00000000..fd36f41f --- /dev/null +++ b/src/weather.py @@ -0,0 +1,128 @@ +#!/usr/bin/python3 +from gi.repository import Gtk, Pango, Gio + +from baseWindow import BaseWindow +from floating import Floating +from util import settings, trackers +from util.geojs import GeoJSLocationProvider +from util.location import LocationData +from util.openweathermap import OWMWeatherProvider + +ICON_SIZE = 128 # probably works OK on most screens + + +class WeatherWidget(Floating, BaseWindow): + """ + WeatherWidget displays current weather on screen + + It is a child of the Stage's GtkOverlay, and its placement is + controlled by the overlay's child positioning function. + + When not Awake, it positions itself around all monitors + using a timer which randomizes its halign and valign properties + as well as its current monitor. + """ + + def __init__(self, initial_monitor=0, low_res=False): + super(WeatherWidget, self).__init__(initial_monitor, Gtk.Align.CENTER, Gtk.Align.START) + self.get_style_context().add_class("weather") + self.set_property("margin", 6) + + self.low_res = low_res + + if not settings.get_show_weather(): + return + + # overall container + big_box = Gtk.Box(Gtk.Orientation.HORIZONTAL) + self.add(big_box) + big_box.show() + + # icon + self.icon_size = ICON_SIZE + self.condition_icon = Gtk.Image() + self.condition_icon.set_size_request(self.icon_size, self.icon_size) + big_box.pack_start(self.condition_icon, False, False, 6) + self.condition_icon.show() + + # temp + condition + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + big_box.pack_start(box, True, False, 6) + box.show() + + self.temp_label = Gtk.Label() + self.temp_label.show() + self.temp_label.set_line_wrap(True) + self.temp_label.set_alignment(0.5, 0.5) + + box.pack_start(self.temp_label, True, False, 6) + + self.desc_label = Gtk.Label() + self.desc_label.show() + self.desc_label.set_line_wrap(True) + self.desc_label.set_alignment(0.5, 0.5) + + if self.low_res: + self.desc_label.set_max_width_chars(50) + else: + self.desc_label.set_max_width_chars(80) + + box.pack_start(self.desc_label, True, True, 6) + + # TODO: get from settings once other providers are available + self.location_provider = GeoJSLocationProvider() + self.weather_provider = OWMWeatherProvider() + + self.location = self.get_location() + self.update_weather() + + trackers.timer_tracker_get().start_seconds("weather", 600, self.update_weather) + + def get_location(self): + loc_string = settings.get_weather_location() + if loc_string == "" or "," not in loc_string: + return self.location_provider.GetLocation() + lat = float(loc_string.split(",")[0]) + lon = float(loc_string.split(",")[1]) + return LocationData(lat, lon) + + def update_weather(self): + desc_font = Pango.FontDescription.from_string(settings.get_message_font()) + weather_font = Pango.FontDescription.from_string(settings.get_weather_font()) + + if self.low_res: + desc_size = desc_font.get_size() * 0.66 + desc_font.set_size(int(desc_size)) + + weather_data = self.weather_provider.GetWeather(self.location) + + in_str = " " + _("in") + " " + + temp = ( + weather_data.temp_f() + if settings.get_weather_units() == "imperial" + else weather_data.temp_c() + ) + temp_string = str(round(temp)) + desc_message = ( + weather_data.condition.description.title() + + in_str + + weather_data.location.city.capitalize() + ) + + markup = '%s\n ' % ( + desc_font.to_string(), + desc_message, + ) + + self.temp_label.set_markup( + '%s°' % (weather_font.to_string(), temp_string) + ) + self.desc_label.set_markup(markup) + gicon = Gio.ThemedIcon.new_from_names(weather_data.condition.icons) + self.condition_icon.set_from_gicon(gicon, Gtk.IconSize.DIALOG) + self.condition_icon.set_pixel_size(self.icon_size) + + @staticmethod + def on_destroy(data=None): + trackers.timer_tracker_get().cancel("weather")