Raindar, desktop Weather App

Pulling data from a remote API
Heads up! You've already completed this tutorial.

Get up-to-date weather direct to your desktop, including meterological data and week-ahead predictions.

The OpenWeatherMap API

Requests to the API can take a few moments to complete. If we perform these in the main application loop this will cause our app to hang while waiting for data. To avoid this we perform all requests in seperate worker threads,

This worker collects both the current weather and a forecast, and returns this to the main thread to update the UI.

First we define a number of custom signals which the worker can emit. These include finished a generic signal for the worker completing, error which emits an Exception message should an error occur and result which returns the result of the API call. The data is returned as two separate dict objects, one representing the current weather and one for the forecast.

python
:::python
class WorkerSignals(QObject):
    '''
    Defines the signals available from a running worker thread.
    '''
    finished = pyqtSignal()
    error = pyqtSignal(str)
    result = pyqtSignal(dict, dict)

The WeatherWorker runnable handles the actual requests to the API. It is initialized with a single parameter location which gives the location that the worker will retrieve the weather data for. Each worker performs two requests, one for the weather, and one for the forecast, receiving a JSON strings from the OpenWeatherMap.org. These are then unpacked into dict objects and emitted using the .result signal.

python
:::python
class WeatherWorker(QRunnable):
    '''
    Worker thread for weather updates.
    '''
    signals = WorkerSignals()
    is_interrupted = False

    def __init__(self, location):
        super().__init__()
        self.location = location

    @pyqtSlot()
    def run(self):
        try:
            params = dict(
                q=self.location,
                appid=OPENWEATHERMAP_API_KEY
            )

            url = 'http://api.openweathermap.org/data/2.5/weather?%s&units=metric' % urlencode(params)
            r = requests.get(url)
            weather = json.loads(r.text)

            # Check if we had a failure (the forecast will fail in the same way).
            if weather['cod'] != 200:
                raise Exception(weather['message'])

            url = 'http://api.openweathermap.org/data/2.5/forecast?%s&units=metric' % urlencode(params)
            r = requests.get(url)
            forecast = json.loads(r.text)

            self.signals.result.emit(weather, forecast)

        except Exception as e:
            self.signals.error.emit(str(e))

        self.signals.finished.emit()

The User Interface

The Raindar UI was created using Qt Designer, and saved as .ui file, which is available for download. This was converted to an importable Python file using pyuic5.

With the main window layout defined in Qt Designer. To create the mainwindow we simply create a subclass of Ui_MainWindow (and QMainWindow) and call self.setupUi(self) as normal.

Create GUI Applications with Python & Qt6 by Martin Fitzpatrick — (PyQt6 Edition) The hands-on guide to making apps with Python — Over 10,000 copies sold!

More info Get the book

To trigger the request for weather data using the push button we connect it's pressed signal to our custom update_weather slot.

Finally we create our thread pool class, to handle running our workers and show the main window.

python
:::python
class MainWindow(QMainWindow, Ui_MainWindow):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setupUi(self)
        self.pushButton.pressed.connect(self.update_weather)
        self.threadpool = QThreadPool()
        self.show()

Requesting and Refreshing data

Pressing the button triggers the update_weather slot method. This creates a new WeatherWorker instance, passing in the currently set location from the lineEdit box. The result and error signals of the worker are connected up to the weather_result handler, and to our custom alert handler respectively.

The alert handler uses QMessageBox to display a message box window, containing the error from the worker.

python
:::python
    def update_weather(self):
        worker = WeatherWorker(self.lineEdit.text())
        worker.signals.result.connect(self.weather_result)
        worker.signals.error.connect(self.alert)
        self.threadpool.start(worker)

    def alert(self, message):
        alert = QMessageBox.warning(self, "Warning", message)

Handling the result

The weather and forecast dict objects returned by the workers are emitted through the result signal. This signal is connected to our custom slot weather_result, which receives the two dict objects. This method is responsible for updating the UI with the result returned, showing both the numeric data and updating the weather icons.

The weather results are updated to the UI by setText on the defined QLabels, formatted to decimal places where appropriate.

python
:::python
    def weather_result(self, weather, forecasts):
        self.latitudeLabel.setText("%.2f °" % weather['coord']['lat'])
        self.longitudeLabel.setText("%.2f °" % weather['coord']['lon'])

        self.windLabel.setText("%.2f m/s" % weather['wind']['speed'])

        self.temperatureLabel.setText("%.1f °C" % weather['main']['temp'])
        self.pressureLabel.setText("%d" % weather['main']['pressure'])
        self.humidityLabel.setText("%d" % weather['main']['humidity'])

        self.weatherLabel.setText("%s (%s)" % (
            weather['weather'][0]['main'],
            weather['weather'][0]['description']
        )

The timestamps are processed using a custom from_ts_to_time_of_day function to return a user-friendlier time of day in am/pm format with no leading zero.

python
:::python
        def from_ts_to_time_of_day(ts):
            dt = datetime.fromtimestamp(ts)
            return dt.strftime("%I%p").lstrip("0")

        self.sunriseLabel.setText(from_ts_to_time_of_day(weather['sys']['sunrise']))

The OpenWeatherMap.org has a custom mapping for icons, with each weather state indicated by a specific number — the full mapping is available here. We're using the free fugue icon set, which has a pretty complete set of weather-related icons. To simplify the mapping between the OpenWeatherMap.org and the icon set, the icons have been renamed to their respective OpenWeatherMap.org numeric code.

python
:::python
        def set_weather_icon(self, label, weather):
            label.setPixmap(
                QPixmap(
                    os.path.join('images', "%s.png" % weather[0]['icon'])
                        )
            )

First we set the current weather icon, from the weather dict, then iterate over the first 5 of the provided forecasts. The forecast icons, times and temperature labels were defined in Qt Designer with the names forecastIcon<n>, forecastTime<n> and forecastTemp<n>, making it simple to iterate over them in turn and retrieve them using getattr with the current iteration index.

python
:::python
        self.set_weather_icon(self.weatherIcon, weather['weather'])

        for n, forecast in enumerate(forecasts['list'][:5], 1):
            getattr(self, 'forecastTime%d' % n).setText(from_ts_to_time_of_day(forecast['dt']))
            self.set_weather_icon(getattr(self, 'forecastIcon%d' % n), forecast['weather'])
            getattr(self, 'forecastTemp%d' % n).setText("%.1f °C" % forecast['main']['temp'])

The full source is available on Github.

Challenges

A few simple ways you could extend this application —

  1. Eliminate repeated requests for the data, by using request_cache. This will persist the request data between runs.
  2. Support for multiple locations.
  3. Configurable forecast length.
  4. Make the current weather available on a toolbar icon while running.
Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak

Raindar, desktop Weather App was written by Martin Fitzpatrick .

Martin Fitzpatrick has been developing Python/Qt apps for 8 years. Building desktop applications to make data-analysis tools more user-friendly, Python was the obvious choice. Starting with Tk, later moving to wxWidgets and finally adopting PyQt.