Skip to content

Commit

Permalink
Alternative AQI provider: AQICN.org #8
Browse files Browse the repository at this point in the history
  • Loading branch information
pskowronek committed Apr 24, 2020
1 parent 4330598 commit d6386a7
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 30 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ _Language versions:_\
This is a forked project of [waveshare-clock](https://github.com/prehensile/waveshare-clock) that only displayed clock and weather and supported only Waveshare 4.2inch B&W displays.
This project enhances the original project to support Waveshare 2.7inch displays with red dye (BWR) and adds the following additional features:
- gauges for current traffic drive times for two configured destinations (thanks to [Google Maps API](https://developers.google.com/maps/documentation/))
- gauge for air quality index (AQI) of home location (thanks to [Airly.eu API](http:https://developer.airly.eu/))
- gauge for air quality index (AQI) of home location (thanks to [Airly.eu API](http:https://developer.airly.eu/) or [World Air Quality Index API](https://aqicn.org))
- weather gauge can display:
- current temperature + weather status icon plus forecast: daily min/max temperatures (thanks to [OpenWeather API](https://openweathermap.org))
- ~~alerts issued by governmental authorities - it works for the EU, US & Canada (thanks to [DarkSky.net API](https://darksky.net/dev/docs))~~ DarkSky is being phased out - see below
Expand Down Expand Up @@ -63,6 +63,8 @@ More photos of the assembled e-paper 2.7inch display sitting on top of Raspberry
- **DarkSky has been recently acquired by Apple since then no new submissions are being accepted**
- DarkSky is now in fallback mode (if not OpenWeather key is provided) and it's been set as deprecated (should work until the end of 2021)
- a key for Air Quality Index data from Airly.eu - you can get it [here](https://developer.airly.eu/register) *)
- alternatively you can use World Air Quality Index API - you can get it [here](https://aqicn.org/data-platform/token/) *)
- WAQI (aqicn.org) works as a fallback if you don't specify the token for Airly
- type of e-paper device, whether it is 2.7 or 4.2 (by default it is pre-configured for 2.7" BWR)
- tweak additional settings to:
- prefer local temperature readings as served by Airly instead of weather provider(s)
Expand Down
34 changes: 20 additions & 14 deletions drawing.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,17 +163,17 @@ def draw_text_eta(self, x, y, text, text_size, draw, font_color=255):
draw.text((x, y), unicode(text, "utf-8"), font=font, fill=font_color)


def draw_airly(self, black_buf, red_buf, airly, black_on_red):
def draw_aqi(self, black_buf, red_buf, aqi, black_on_red):
start_pos = (0, 100)
no_warn = airly.aqi < self.aqi_warn_level
no_warn = aqi.aqi < self.aqi_warn_level
buf = black_buf if no_warn else red_buf

back = Image.open('./resources/images/back_aqi.bmp')
buf.paste(back, start_pos)

draw = ImageDraw.Draw(buf)

caption = "%3i" % int(round(airly.aqi))
caption = "%3i" % int(round(aqi.aqi))
if not no_warn and black_on_red:
black_draw = ImageDraw.Draw(black_buf)
self.draw_text_aqi(start_pos[0] + 25, start_pos[1] - 5, caption, 90, black_draw, 0)
Expand Down Expand Up @@ -210,18 +210,24 @@ def draw_shutdown(self, is_mono):
return black_buf, red_buf


def draw_airly_details(self, airly):
def draw_aqi_details(self, aqi):
black_buf = Image.new('1', (self.CANVAS_WIDTH, self.CANVAS_HEIGHT), 1)
red_buf = Image.new('1', (self.CANVAS_WIDTH, self.CANVAS_HEIGHT), 1)
draw = ImageDraw.Draw(black_buf)
self.draw_text(10, 10, "Air Quality Index by Airly.eu", 35, draw)

y = self.draw_text(10, 60, "PM2.5: {:0.0f}, PM10: {:0.0f} ({})".format(airly.pm25, airly.pm10, self.PM_SYMBOL.encode('utf-8')), 30, draw)
y = self.draw_text(10, y, "AQI: {:0.0f}, level: {}".format(airly.aqi, airly.level.replace('_', ' ').encode('utf-8')), 30, draw)
y = self.draw_multiline_text(10, y, "Advice: {}".format(airly.advice.encode('utf-8')), 25, draw)
y = self.draw_text(10, y, "Hummidity: {} %".format(airly.hummidity), 30, draw)
y = self.draw_text(10, y, "Pressure: {} hPa".format(airly.pressure), 30, draw)
y = self.draw_text(10, y, "Temperature: {} {}C".format(airly.temperature, self.TEMPERATURE_SYMBOL.encode('utf-8')), 30, draw)
provider = 'Airly.eu' if 'providers.airly.Airly' in str(type(aqi)) else 'AQICN'
self.draw_text(10, 10, "Air Quality Index by {}".format(provider), 35, draw)

y = self.draw_text(10, 60, "PM2.5: {:0.0f}, PM10: {:0.0f} ({})".format(aqi.pm25, aqi.pm10, self.PM_SYMBOL.encode('utf-8')), 30, draw)
self.draw_text(10, y, "AQI: {:0.0f}, level: {}".format(aqi.aqi, aqi.level.replace('_', ' ').encode('utf-8') if aqi.level else 'N/A'), 30, draw)
if aqi.advice:
y = self.draw_multiline_text(10, y, "Advice: {}".format(aqi.advice.encode('utf-8')) if aqi.advice else 'N/A', 25, draw)
if aqi.hummidity != -1:
y = self.draw_text(10, y, "Hummidity: {} %".format(aqi.hummidity), 30, draw)
if aqi.pressure != -1:
y = self.draw_text(10, y, "Pressure: {} hPa".format(aqi.pressure), 30, draw)
if aqi.temperature:
y = self.draw_text(10, y, "Temperature: {} {}C".format(aqi.temperature, self.TEMPERATURE_SYMBOL.encode('utf-8')), 30, draw)

return black_buf, red_buf

Expand Down Expand Up @@ -287,7 +293,7 @@ def draw_system_details(self, sys_info):
return black_buf, red_buf


def draw_frame(self, is_mono, formatted_time, use_hrs_mins_separator, weather, prefer_airly_local_temp, black_on_red, airly, gmaps1, gmaps2):
def draw_frame(self, is_mono, formatted_time, use_hrs_mins_separator, weather, prefer_airly_local_temp, black_on_red, aqi, gmaps1, gmaps2):
black_buf = Image.new('1', (self.CANVAS_WIDTH, self.CANVAS_HEIGHT), 1)

# for mono display we simply use black buffer so all the painting will be done in black
Expand All @@ -303,10 +309,10 @@ def draw_frame(self, is_mono, formatted_time, use_hrs_mins_separator, weather, p
self.draw_eta(1, black_buf, red_buf, gmaps2, self.secondary_time_warn_above, black_on_red)

# draw AQI into buffer
self.draw_airly(black_buf, red_buf, airly, black_on_red)
self.draw_aqi(black_buf, red_buf, aqi, black_on_red)

# draw weather into buffer
self.draw_weather(black_buf, red_buf, weather, airly, prefer_airly_local_temp, black_on_red)
self.draw_weather(black_buf, red_buf, weather, aqi, prefer_airly_local_temp, black_on_red)

return black_buf, red_buf

Expand Down
34 changes: 22 additions & 12 deletions epaper.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from drawing import Drawing
from providers.airly import Airly
from providers.aqicn import Aqicn
from providers.darksky import DarkSky
from providers.openweather import OpenWeather
from providers.gmaps import GMaps
Expand Down Expand Up @@ -55,12 +56,21 @@ class EPaper(object):
int(os.environ.get("SECONDARY_TIME_WARN_ABOVE_PERCENT", "50"))
)

airly = Airly(
os.environ.get("AIRLY_KEY"),
os.environ.get("LAT"),
os.environ.get("LON"),
int(os.environ.get("AIRLY_TTL", "20"))
)
aqi = None
if os.environ.get("AIRLY_KEY"):
aqi = Airly(
os.environ.get("AIRLY_KEY"),
os.environ.get("LAT"),
os.environ.get("LON"),
int(os.environ.get("AIRLY_TTL", "20"))
)
else:
aqi = Aqicn(
os.environ.get("AQICN_KEY"),
os.environ.get("AQICN_CITY_OR_ID"),
int(os.environ.get("AQICN_TTL", "20"))
)

weather = None
if os.environ.get("OPENWEATHER_KEY"):
weather = OpenWeather(
Expand Down Expand Up @@ -162,9 +172,9 @@ def display_shutdown(self):
self.display_buffer(black_frame, red_frame, 'shutdown')


def display_airly_details(self):
black_frame, red_frame = self.drawing.draw_airly_details(self.airly.get())
self.display_buffer(black_frame, red_frame, 'airly')
def display_aqi_details(self):
black_frame, red_frame = self.drawing.draw_aqi_details(self.aqi.get())
self.display_buffer(black_frame, red_frame, 'aqi')


def display_gmaps_details(self):
Expand Down Expand Up @@ -197,8 +207,8 @@ def display_main_screen(self, dt, force = False):
weather_data = self.weather.get()
logging.info("--- weather: " + json.dumps(weather_data))

airly_data = self.airly.get()
logging.info("--- airly: " + json.dumps(airly_data))
aqi_data = self.aqi.get()
logging.info("--- aqi: " + json.dumps(aqi_data))

gmaps1_data = self.gmaps1.get()
logging.info("--- gmaps1: " + json.dumps(gmaps1_data))
Expand All @@ -213,7 +223,7 @@ def display_main_screen(self, dt, force = False):
weather_data,
self.PREFER_AIRLY_LOCAL_TEMP,
self.WARN_PAINTED_BLACK_ON_RED,
airly_data,
aqi_data,
gmaps1_data,
gmaps2_data
)
Expand Down
4 changes: 2 additions & 2 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def action_button(key, epaper):
if key == 1:
details_to_display = lambda: epaper.display_gmaps_details()
elif key == 2:
details_to_display = lambda: epaper.display_airly_details()
details_to_display = lambda: epaper.display_aqi_details()
elif key == 3:
details_to_display = lambda: epaper.display_weather_details()
elif key == 4:
Expand All @@ -129,7 +129,7 @@ def refresh_main_screen(epaper, force = False):
epaper.display_main_screen(utc_dt.astimezone(get_localzone()), force)
if DEBUG_MODE:
epaper.display_weather_details()
epaper.display_airly_details()
epaper.display_aqi_details()
epaper.display_gmaps_details()
epaper.display_system_details()

Expand Down
2 changes: 1 addition & 1 deletion providers/airly.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def get(self):
try:
airly_data = self.load()
if airly_data is None or not airly_data["current"] or not airly_data["current"]["values"]:
logging.warn("No reasonable data returned by Airly. Check API key (status code) or whether the location has any sesnors around (visit https://airly.eu/map/en/)")
logging.warn("No reasonable data returned by Airly. Check API key (status code) or whether the location has any sensors around (visit: https://airly.eu/map/en/)")
return self.DEFAULT

return AirlyTuple(
Expand Down
72 changes: 72 additions & 0 deletions providers/aqicn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# AQICN.org (Air Quality Open Data Platform) - a provider of AQI througout a world (an alternative /a fallback/ for Airly which is more popular in Central Europe)

from acquire import Acquire

import logging
import requests
from collections import namedtuple


AqicnTuple = namedtuple('Aqicn', ['pm25', 'pm10', 'hummidity', 'pressure', 'temperature', 'aqi', 'level', 'advice'])


class Aqicn(Acquire):


DEFAULT = AqicnTuple(pm25=-1, pm10=-1, pressure=-1, hummidity=-1, temperature=None, aqi=-1, level='n/a', advice='n/a')


def __init__(self, key, city_or_id, cache_ttl):
self.key = key
self.city_or_id = city_or_id
self.cache_ttl = cache_ttl


def cache_name(self):
return "aqicn.json"


def ttl(self):
return self.cache_ttl


def acquire(self):
logging.info("Getting a Aqicn status from the internet...")

try:
r = requests.get(
"https://api.waqi.info/feed/{}/?token={}".format(
self.city_or_id,
self.key
)
)
return r
except Exception as e:
logging.exception(e)

return None


def get(self):
try:
aqicn_data = self.load()
if aqicn_data is None or aqicn_data["status"] != 'ok':
logging.warn("No reasonable data returned by Aqicn. Check API key (status code) or whether the city or id is known by the service (visit: https://aqicn.org/search/)")
return self.DEFAULT

return AqicnTuple(
pm25=aqicn_data["data"]["iaqi"]["pm25"]["v"],
pm10=-1,
pressure=-1,
hummidity=-1,
temperature=None,
aqi=aqicn_data["data"]["aqi"],
level=None,
advice=None
)

except Exception as e:
logging.exception(e)
return self.DEFAULT


6 changes: 6 additions & 0 deletions run-EDIT-ME.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export GOOGLE_MAPS_KEY=GET_YOUR_OWN_KEY # get the key from: https://develope
export OPENWEATHER_KEY=GET_YOUR_OWN_KEY # get the key from: https://openweathermap.org/home/sign_up
# A key for AQI (Air Quality Index) from AIRLY.EU API (data for certain countries only, as yet, but you may order their device to provide data also for your neighbours)
export AIRLY_KEY=GET_YOUR_OWN_KEY # get the key from: https://developer.airly.eu/register
# A key for AQI (Air Quality Index) from AQICN API (data for cities across the world) - used as a fallback if Airly key above is not defined. Please also specify AQICN_CITY_OR_ID below.
export AQICN_KEY=GET_YOUR_OWN_KEY # get the key from: https://aqicn.org/data-platform/token/


# A key for weather forecasts from DarkSky.net API (deprecated)
export DARKSKY_KEY=GET_YOUR_OWN_KEY # get the key from: https://darksky.net/dev/register
Expand All @@ -41,9 +44,12 @@ export DARKSKY_KEY=GET_YOUR_OWN_KEY # get the key from: https://darksky.
# Cache TTLs in minutes for each data fetcher (refer to free accounts limitations before you change the values any lower than 10m)
export GOOGLE_MAPS_TTL=10
export AIRLY_TTL=20
export AQICN_TTL=20
export OPENWEATHER_TTL=15
export DARKSKY_TTL=15 # deprecated

# AQICN City or ID (see: https://aqicn.org/search/)
export AQICN_CITY_OR_ID=krakow

# Units
export GOOGLE_MAPS_UNITS=metric # refer to: https://developers.google.com/maps/documentation/distance-matrix/intro#unit_systems for allowed values (metric, imperial)
Expand Down

0 comments on commit d6386a7

Please sign in to comment.