Raindar
Desktop daily weather, forecast app in PyQt

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.

API key

Before running the application you need to obtain a API key from OpenWeatherMap.org. This key is unique to you, and should be kept secret (don't add it to your code). To use a key with Raindar, you need to make it available in the environment variable OPENWEATHERMAP_API_KEY.

On Linux or Mac you can do this by using export in the shell.

export OPENWEATHERMAP_API_KEY=<your key here>

The key is then obtained from the environment by the following line in our code. The variable OPENWEATHERMAP_API_KEY will now contain your API key can be be used for requests.

OPENWEATHERMAP_API_KEY = os.environ.get('OPENWEATHERMAP_API_KEY')

"""
Get an API key from https://openweathermap.org/ to use with this
application.

"""

Requesting data

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.

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.

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

    def __init__(self, location):
        super(WeatherWorker, self).__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 MainWindow

The main window layout is 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.

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.

class MainWindow(QMainWindow, Ui_MainWindow):

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

Refresh 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.

    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.

    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.

        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.

        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.

        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.

Want to build your own apps?

Then you might enjoy this book! Create Simple GUI Applications with Python & Qt is my guide to building cross-platform GUI applications with Python. Work step by step from displaying your first window to building fully functional desktop software.

Improvements

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.

Continue reading

7Pez  gui

This is a functionally terrible unzip application, saved only by the fact that you get to look at a cat while using it. The original idea reflected in the name 7Pez was actually worse — to rig it up so you had to push on the head to unzip each file ... More

Discussion