Last active
September 2, 2020 05:32
-
-
Save sorz/3ea5050185bd4518aff464eef830e778 to your computer and use it in GitHub Desktop.
Simple program that shows AQI on system tray as a colored pie chart.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
"""Simple program that shows AQI on system tray as a colored pie chart. | |
It fetches PM_2.5 data via Graphite's HTTP API every minutes. | |
Required Python packages: | |
requests, pystray, Pillow | |
""" | |
import time | |
import webbrowser | |
from typing import Optional | |
from threading import Thread | |
from pystray import Icon, Menu, MenuItem | |
from PIL import Image, ImageDraw | |
import requests | |
ICON_SIZE = 32 | |
MARGIN = 1 | |
DRAW_AREA = [MARGIN, MARGIN, ICON_SIZE - MARGIN * 2, ICON_SIZE - MARGIN * 2] | |
REFRESH_INTERVAL_SECS = 60 | |
# Change the following constants, pointing to your sensors. | |
WEB_URL = 'https://example.com/grafana/d/YOUR-DASHBOARD/' | |
GRAPHITE_URL = 'https://example.com/graphite-api/' | |
TARGET = 'your.sensor.pm25' | |
FROM = '-5min' | |
US_AQI = ( | |
[350.5, 500, 0.66, 501, '#800000', 'Hazardous'], | |
[150.5, 350.5, 0.99, 201, '#800080', 'Very Unhealthy'], | |
[55.5, 150.5, 0.5185, 151, '#e60000', 'Unhealthy'], | |
[35.5, 55.5, 2.45, 101, '#ffa500', 'Unhealthy for Sensitive Groups'], | |
[12, 35.5, 2.085, 51, '#e6e600', 'Moderate'], | |
[0, 12, 4.1667, 0, '#008000', 'Good'] | |
); | |
def create_tray_icon(pm25: Optional[int]=None) -> (Image, str): | |
img = Image.new('RGBA', (ICON_SIZE, ICON_SIZE), (0, 0, 0, 0)) | |
draw = ImageDraw.Draw(img) | |
box = [0, 0, ICON_SIZE, ICON_SIZE] | |
if pm25 is None: | |
color = '#888888' | |
deg = 0 | |
title = 'AQI Unknown' | |
else: | |
for lower, upper, coe, base, col, category in US_AQI: | |
if pm25 > lower: | |
color = col | |
aqi = (pm25 - lower) * coe + base | |
deg = (pm25 - lower) / (upper - lower) * 360 | |
title = f'AQI {aqi:.0f} - {category}' | |
break | |
draw.pieslice(DRAW_AREA, 0, 360, f'{color}80') | |
draw.pieslice(DRAW_AREA, 0, deg, f'{color}ff') | |
img = img.rotate(90) | |
return img, title | |
class App: | |
def __init__(self): | |
self.session = requests.Session() | |
img, title = create_tray_icon() | |
menu = Menu( | |
MenuItem('&Refresh', self.refresh, default=True), | |
MenuItem('&Open in browser', self.open_browser), | |
Menu.SEPARATOR, | |
MenuItem('&Quit', self.quit), | |
) | |
self.mainloop = Thread(target=self.loop, daemon=True) | |
self.icon = Icon("AQI", img, title, menu) | |
self.icon.run(setup=lambda _: self.mainloop.start()) | |
def set_tray_icon(self, pm25: Optional[int]): | |
self.icon.icon, self.icon.title = create_tray_icon(pm25) | |
def loop(self): | |
self.icon.visible = True | |
while True: | |
try: | |
pm25 = self.fetch_pm25() | |
self.set_tray_icon(pm25) | |
except IOError: | |
self.set_tray_icon(None) | |
time.sleep(REFRESH_INTERVAL_SECS) | |
def refresh(self): | |
self.set_tray_icon() | |
pm25 = self.fetch_pm25() | |
self.set_tray_icon(pm25) | |
def fetch_pm25(self) -> Optional[int]: | |
url = f'{GRAPHITE_URL}render?target={TARGET}&from={FROM}&format=json' | |
resp = self.session.get(url) | |
if not resp.ok: | |
return | |
points = [v for v, _ in resp.json()[0]['datapoints'] if v is not None] | |
if not points: | |
return | |
avg = sum(points) / len(points) | |
return avg | |
def open_browser(self): | |
webbrowser.open(WEB_URL) | |
def quit(self): | |
self.icon.stop() | |
if __name__ == '__main__': | |
App() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment