Skip to content

Commit

Permalink
Display noteworthy weather conditions as a third line in event summar…
Browse files Browse the repository at this point in the history
…ies.

This is very quick and dirty. Right now we display the temperature if it is outside of the range 65‒85℉ / 18‒29℃, and any free-form forecast text that contains the words "rain" or "snow".

A followup will extend this to include warnings and alerts (like "Winter Storm Warning: Heavy Mixed precipitation, Mixed Precipitation"). This should probably be displayed in groups in general, though, rather than just as additional weather exceptions (both because the information is more pressing and because alerts aren't really forecasted).

See #80.
  • Loading branch information
nmlorg committed Dec 1, 2019
1 parent ad86b69 commit 6d304fd
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 1 deletion.
5 changes: 5 additions & 0 deletions metabot/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ def _dont_mangle_callback_data(monkeypatch):
monkeypatch.setattr('ntelebot.keyboardutil.fix', lambda keyboard, maxlen=0: None)


@pytest.fixture(autouse=True)
def _disable_geoutil(monkeypatch):
monkeypatch.setattr('metabot.util.geoutil._CLIENT', None)


def _format_message(response):
text = response.pop('text', None) or response.pop('caption', '(EMPTY MESSAGE)')
reply_markup = response.pop('reply_markup', None)
Expand Down
23 changes: 23 additions & 0 deletions metabot/util/eventutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import pytz

from metabot.util import geoutil
from metabot.util import html
from metabot.util import humanize
from metabot.util import tickets
Expand Down Expand Up @@ -52,11 +53,33 @@ def format_event(bot, event, tzinfo, full=True):
'q': event['location'].encode('utf-8'),
}) # yapf: disable
message = '%s @ <a href="%s">%s</a>' % (message, location_url, location_name)
geo = format_geo(event['location'], event['start'])
if geo:
message = '%s\n\u26a0 %s' % (message, geo)
if full and event['description']:
message = '%s\n\n%s' % (message, html.sanitize(event['description']))
return message


def format_geo(address, now):
"""Build a string of weather exceptions for the given address as of the given time."""

geo = geoutil.lookup(address, now)
if not geo or not geo.get('forecast'):
return
warnings = []
if geo['forecast']['temperatureUnit'] == 'F':
if not 65 <= geo['forecast']['temperature'] <= 85:
warnings.append('%s\u2109' % geo['forecast']['temperature'])
elif geo['forecast']['temperatureUnit'] == 'C':
if not 18 <= geo['forecast']['temperature'] <= 29:
warnings.append('%s\u2103' % geo['forecast']['temperature'])
short = geo['forecast']['shortForecast'].lower()
if 'rain' in short or 'snow' in short:
warnings.append(geo['forecast']['shortForecast'])
return ' \u2022 '.join(warnings)


def humanize_range(start, end, tzinfo):
"""Return the range between start and end as human-friendly text."""

Expand Down
98 changes: 98 additions & 0 deletions metabot/util/geoutil.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""Quick utilities for geocoding an address and looking up its weather forecast."""

import logging
import time
import urllib.parse

import googlemaps
import requests

from metabot.util import iso8601
from metabot.util import pickleutil

try:
_CLIENT_KEY = next(open('config/google_maps_apikey')).strip()
except IOError:
_CLIENT = None
else:
_CLIENT = googlemaps.Client(key=_CLIENT_KEY)
_CACHEFILE = 'config/geoutil.pickle'
_CACHE = pickleutil.load(_CACHEFILE) or {}
_SHORTCACHE = {}


def _save():
pickleutil.dump(_CACHEFILE, _CACHE)


def geocode(address):
"""Look up the given address in Google Maps."""

if 'geocode' not in _CACHE:
_CACHE['geocode'] = {}
if address not in _CACHE['geocode']:
_CACHE['geocode'][address] = _CLIENT.geocode(address)
_save()
return _CACHE['geocode'][address]


def _weatherfetch(url):
url = urllib.parse.urljoin('https://api.weather.gov/', url)
headers = {
'user-agent': 'https://github.com/nmlorg/metabot',
}
logging.info('Fetching %r.', url)
return requests.get(url, headers=headers).json()


def weatherpoint(lat, lon):
"""Map a latitude/longitude to a weather.gov gridpoint."""

# https://weather-gov.github.io/api/general-faqs#how-do-i-get-a-forecast-for-a-location-from-the-api
lat = ('%.4f' % lat).rstrip('0')
lon = ('%.4f' % lon).rstrip('0')
key = '%s,%s' % (lat, lon)

if 'weatherpoint' not in _CACHE:
_CACHE['weatherpoint'] = {}
if key not in _CACHE['weatherpoint']:
_CACHE['weatherpoint'][key] = _weatherfetch('points/' + key)
_save()
return _CACHE['weatherpoint'][key]


def hourlyforecast(lat, lon):
"""Retrieve the hourly forecast for the given latitude/longitude."""

point = weatherpoint(lat, lon)
if not point.get('properties'):
return ()
now = time.time()
url = point['properties']['forecastHourly']
last, ret = _SHORTCACHE.get(url) or (0, None)
if last < now - 10 * 60:
ret = _weatherfetch(url)['properties']['periods']
_SHORTCACHE[url] = (now, ret)
return ret


def lookup(address, now=None):
"""Retrieve the forecasted weather conditions for the given address as of the given time."""

if _CLIENT is None:
return
if now is None:
now = time.time()
ret = {}
geo = geocode(address)
if not geo:
return
ret['lat'] = geo[0]['geometry']['location']['lat']
ret['lon'] = geo[0]['geometry']['location']['lng']
for period in hourlyforecast(ret['lat'], ret['lon']):
start = iso8601.totimestamp(period['startTime'])
end = iso8601.totimestamp(period['endTime'])
if start <= now < end:
ret['forecast'] = period
break
return ret
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

setuptools.setup(
name='metabot',
version='0.3.16.4',
version='0.3.17',
author='Daniel Reed',
author_email='[email protected]',
description='Modularized, multi-account bot.',
Expand All @@ -14,6 +14,7 @@
package_data={'': ['*.html']},
python_requires='>=3.5',
install_requires=[
'googlemaps',
'ntelebot >= 0.3.4',
'pytz',
'PyYAML >= 5.1',
Expand Down

0 comments on commit 6d304fd

Please sign in to comment.