Skip to content

Instantly share code, notes, and snippets.

@sorz
Last active Sep 2, 2020
Embed
What would you like to do?
Simple program that shows AQI on system tray as a colored pie chart.
#!/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