Skip to content

Instantly share code, notes, and snippets.

@sveatlo
Last active December 16, 2023 13:28
Show Gist options
  • Save sveatlo/d2fb837758088a98ad727f56072d6f1c to your computer and use it in GitHub Desktop.
Save sveatlo/d2fb837758088a98ad727f56072d6f1c to your computer and use it in GitHub Desktop.
Waybar weather module with geolocation

Waybar weather module with geolocation

This is an example set of scripts that allow me to always have weather forecast at my waybar.

Each script can be used on its own, but together they are great.

Requirements

geolocateme.py

Geolocation uses Google's Geolocation API to approximate your location via wifi AP in your surroundings. You must obtain an API key for this to work.

weather.py

Weather information and forecast is from OpenWeatherMap. You need their API key. This script can report weather from either city ID (which you can find using owm find) or latitude+longitude, which can be obtained from the geolocateme.py script or entered manually. The output format can also be fully customized using jinja template supplied to the --output-format argument.

waybar module

This script simlply combines the other to into a format accepted by waybar.

#!/usr/bin/env python3
import typing
import jsons
import requests
import gi
gi.require_version('NM', '1.0')
from gi.repository import NM
from time import sleep
class Location(object):
def __init__(self, latitude, longitude, accuracy = -1):
self.latitude = latitude
self.longitude = longitude
self.accuracy = accuracy
def __str__(self) -> str:
return f"{self.latitude};{self.longitude};{self.accuracy}"
class WifiAP(object):
def __init__(self, nm_ap):
ssid = nm_ap.get_ssid()
ssid = "" if not ssid else NM.utils_ssid_to_utf8(nm_ap.get_ssid().get_data())
self.ssid = ssid
self.channel = NM.utils_wifi_freq_to_channel(nm_ap.get_frequency())
self.macAddress = nm_ap.get_bssid()
self.signalStrength = -1 * nm_ap.get_strength()
class Geolocator(object):
def __init(self, google_api_key: str):
self.google_api_key = google_api_key
pass
def get_location(self) -> Location:
location = self._query_geolocation()
return location
def _query_geolocation(self) -> Location:
wifi_aps = self._get_wifi_aps()
payload = {
"considerIp": "true",
"wifiAccessPoints": wifi_aps,
}
params = {
"key": "<YOUR API KEY>"
}
headers = {'Content-Type': 'application/json'}
r = requests.post("https://www.googleapis.com/geolocation/v1/geolocate", params=params, data=jsons.dumps(payload), headers=headers)
if r.status_code != 200:
raise Exception("error querying geolocation API", r.json())
d = r.json()
l = d['location']
return Location(l['lat'], l['lng'], d['accuracy'])
def _get_wifi_aps(self) -> typing.List[WifiAP]:
aps = []
nmc = NM.Client.new(None)
devs = nmc.get_devices()
for dev in devs:
if dev.get_device_type() != NM.DeviceType.WIFI:
continue
try:
dev.request_scan_async()
except gi.repository.GLib.Error as err:
# Too frequent rescan error
if not err.code == 6: # pylint: disable=no-member
raise err
for dev in devs:
if dev.get_device_type() != NM.DeviceType.WIFI:
continue
for ap in dev.get_access_points():
aps.append(WifiAP(ap))
return aps
if __name__ == "__main__":
g = Geolocator()
print(g.get_location())
"custom/weather": {
"format": "{}",
"format-alt": "{alt}",
"format-alt-click": "click-left",
"interval": 1800,
"return-type": "json",
"exec": "~/.config/waybar/modules/weather-waybar-module.sh",
"exec-if": "ping openweathermap.org -c1",
"signal": 8
},
#!/bin/bash
WEATHER_PATH=$HOME/src/scripts/weather.py
GEOLOCATOR_PATH=$HOME/src/scripts/geolocateme.py
LOCATION=$($GEOLOCATOR_PATH)
LAT=$(echo "$LOCATION" | awk '{split($0,l,";"); print l[1]}')
LON=$(echo "$LOCATION" | awk '{split($0,l,";"); print l[2]}')
$WEATHER_PATH --lat "$LAT" --lon "$LON" --output-format '{"text": "{{current.icon}} {{current.temperature}}°C", "alt": "{{city}}: {{current.temperature}}°C, {{current.description_long}} -> {{next.temperature}}°C, {{next.description_long}}", "tooltip": "{{city}}: {{current.temperature_min}}°C -> {{current.temperature_max}}°C"}'
#!/usr/bin/env python3
import sys
import argparse
import requests
from unidecode import unidecode
from geolocateme import Geolocator
import jinja2
# OpenWeatherMap API key
appid = "<YOUR API KEY>"
# define icons
# icons = {
# 'clear': "",
# 'clouds': "",
# 'rain': "",
# 'thunderstorm': "",
# 'snow': "",
# 'fog': ""
# }
icons = {
# clear
800: '', # clear sky
# clouds
801: '', # few clouds: 11-25%
802: '', # scattered clouds: 25-50%
803: '', # broken clouds: 51-84%
804: '', # overcast clouds: 85-100%
# drizzle
300: '', # light intensity drizzle
301: '', # drizzle
302: '', # heavy intensity drizzle
310: '', # light intensity drizzle rain
311: '', # drizzle rain
312: '', # heavy intensity drizzle rain
313: '', # shower rain and drizzle
314: '', # heavy shower rain and drizzle
321: '', # shower drizzle
# rain
500: '', # light rain
501: '', # moderate rain
502: '', # heavy intensity rain
503: '', # very heavy rain
504: '', # extreme rain
511: '', # freezing rain
520: '', # light intensity shower rain
521: '', # shower rain
522: '', # heavy intensity shower rain
531: '', # ragged shower rain
# thunderstorm
200: '', # thunderstorm with light rain
201: '', # thunderstorm with rain
202: '', # thunderstorm with heavy rain
210: '', # light thunderstorm
211: '', # thunderstorm
212: '', # heavy thunderstorm
221: '', # ragged thunderstorm
230: '', # thunderstorm with light drizzle
231: '', # thunderstorm with drizzle
232: '', # thunderstorm with heavy drizzle
# snow
600: '', # light snow
601: '', # Snow
602: '', # Heavy snow
611: '', # Sleet
612: '', # Light shower sleet
613: '', # Shower sleet
615: '', # Light rain and snow
616: '', # Rain and snow
620: '', # Light shower snow
621: '', # Shower snow
622: '', # Heavy shower snow
# atmosphere
701: '', # mist
711: '', # smoke
721: '', # haze
731: '', # sand/dust whirls
741: '', # fog
751: '', # sand
761: '', # dust
762: '', # volcanic ash
771: '', # sqalls
781: '', # tornado
}
class _WeatherInfo():
def __init__(self, raw_json_data):
raw_weather = raw_json_data["weather"][0]
raw_main = raw_json_data["main"]
self._condition_id = raw_weather["id"]
self.description_short = raw_weather["main"].lower()
self.description_long = raw_weather["description"]
self.temperature = raw_main["temp"]
self.temperature_min = raw_main["temp_min"]
self.temperature_max = raw_main["temp_max"]
self.pressure = raw_main["pressure"]
self.humidity = raw_main["humidity"]
self.icon = icons[self._condition_id]
def __getitem__(self, item):
return getattr(self, item)
class WeatherMan(object):
def __init__(self, owm_api_key, city_id = None, lat = None, lon = None, units = 'metric'):
self._api_key = owm_api_key
self._units = units
self._city_id = city_id
self._gps = (lat,lon)
self.city = ""
self.current = None
self.next = None
if self._city_id is None and (self._gps[0] is None or self._gps[1] is None):
coor = Geolocator().get_location()
self._gps = (coor.latitude, coor.longitude)
self._get_weather()
def _get_weather(self):
params = {
'units': self._units,
'appid': self._api_key,
}
if self._city_id is not None:
params['id'] = self._city_id
else:
params['lat'] = self._gps[0]
params['lon'] = self._gps[1]
r = requests.get(
"http://api.openweathermap.org/data/2.5/forecast", params=params)
d = r.json()
if d['cod'] != '200':
raise Exception("cannot get weather forecast", d['message'])
self.city = d["city"]["name"]
self._city_id = d["city"]["id"] if self._city_id is None else self._city_id
self.current = _WeatherInfo(d["list"][0])
self.next = _WeatherInfo(d["list"][1])
def __getitem__(self, item):
return getattr(self, item)
def main(city_id, lat, lon, template):
weather = WeatherMan(appid, city_id, lat, lon)
t = jinja2.Template(template)
print(t.render(city = weather.city, current = weather.current, next = weather.next))
if __name__ == "__main__":
parser = argparse.ArgumentParser(
add_help=True, description='Print weather informations for a location')
parser.add_argument("--lat", action="store", dest="lat",
default=None, help="GPS latitude")
parser.add_argument("--lon", action="store", dest="lon",
default=None, help="GPS longitude")
parser.add_argument("--city-id", action="store", dest="city_id",
default=None, help="City ID by OpenWeatherMap")
parser.add_argument("--output-format", action="store", dest="output_format",
default="{{city}} - {{current.description_long}} - {{current.temperature}}°C", help="Output format jinja template")
try:
args = parser.parse_args()
except SystemExit as exception:
print(exception)
args, unknown = parser.parse_known_args()
# if (args.city_id != 0 and (args.lat != 0 or args.lon != 0)) or ((args.lat != 0 and args.lon == 0) or (args.lat == 0 and args.lon != 0)):
# print("Invalid parameters")
# sys.exit(1)
main(args.city_id, args.lat, args.lon, args.output_format)
@TheLastTeapot
Copy link

Also, how do I get it to work without geolocation?

@sveatlo
Copy link
Author

sveatlo commented Sep 20, 2022

What do I need to do to get those modules?

pip install jsons unidecode should help you.

Also, how do I get it to work without geolocation?

You can supply the --lat and --lon parameters to get weather by latitude and longitude or --city-id if you know the city ID from OWM (it's shown in the URL).
In case you want to get rid of the dependency on the geolocateme script/module, you can simply remove the import line and raise an error in case of unsupplied location.

no geolocation patch

--- src/scripts/weather.py	2020-09-07 16:16:44.311028180 +0200
+++ /tmp/weather.py	2022-09-20 10:19:51.829511044 +0200
@@ -4,7 +4,6 @@
 import argparse
 import requests
 from unidecode import unidecode
-from geolocateme import Geolocator
 import jinja2
 
 # OpenWeatherMap API key
@@ -123,8 +122,7 @@
         self.next = None
 
         if self._city_id is None and (self._gps[0] is None or self._gps[1] is None):
-            coor = Geolocator().get_location()
-            self._gps = (coor.latitude, coor.longitude)
+            raise Exception("no location for weather")
 
         self._get_weather()

@TheLastTeapot
Copy link

Thank you!

@TheLastTeapot
Copy link

Now it can't get the weather because d['cod'] != '200', and I cannot find anything to fix this

@TheLastTeapot
Copy link

nvm fixed it

@TheLastTeapot
Copy link

Even explicitly setting font awesome as the font most of the icons don't show up. Any idea why?

@jesse-osiecki
Copy link

jesse-osiecki commented Oct 5, 2022

Which version of FontAwesome? V4.7 which ships with Ubuntu doesn't have all of the glyphs

@TheLastTeapot
Copy link

archlinux package version 6.2.0-1

@jesse-osiecki
Copy link

jesse-osiecki commented Oct 6, 2022

Thank you! I just tried with my Archlinux Laptop and had similar missing glyph. Is it possible that you may have one of the pro versions installed?

Copy link

ghost commented Feb 9, 2023

Works amazing, my only question as I figured out how to switch it from metric to imperial (as I am in the US) is there a way to limit the numbers after the decimal, or get rid of them totally?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment