piweather-bom/pi_weather.py

428 lines
20 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
import os
import requests
from datetime import datetime
from dateutil import tz
from papirus import PapirusComposite
from sys import argv
from time import sleep
from weather_au import api
DIRECTORY = os.path.dirname(os.path.realpath(__file__))
class PiWeather:
def __init__(self):
self.config = self.load_config()["weather"]
self.unit = self.get_unit()
self.location = self.get_location()
self.weather = api.WeatherApi(search=self.location)
self.lookup = {}
self.compass_dirs = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
"S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"]
self.compass_dirs_simple = ["N", "NE", "NE", "NE", "E", "SE", "SE", "SE",
"S", "SW", "SW", "SW", "W", "NW", "NW", "NW"]
@staticmethod
def load_config():
"""Load PiWeather Config
Returns:
dict -- Dictonary of config options
"""
with open(os.path.join(DIRECTORY, 'config.json')) as config_file:
return json.load(config_file)
def get_unit(self):
"""Read the selected temperature unit from config
Returns:
str -- String of unit in lowercase
"""
if "unit" in self.config:
return self.config["unit"].lower()
return "c"
def get_location(self):
"""Read the location set in the config
Returns:
str -- String of the location
"""
if len(argv) > 1:
return str(argv[1])
if "location" in self.config:
return self.config["location"]
return "Melbourne"
def get_wind_direction(self, direction):
"""Converts the direction from degrees to compass
Arguments:
direction {int} -- Direction in degrees
Returns:
str -- Compass/Degrees direction depending on config
"""
ix = int((int(direction) + 11.25)/22.5 - 0.02)
if self.config["wind_direction"] == "compass":
return self.compass_dirs[ix % 16]
elif self.config["wind_direction"] == "simplecompass":
return self.compass_dirs_simple[ix % 16]
return direction
@staticmethod
def convert_local(time):
"""Convert UTC to local timezone
Arguments:
time {datetime} -- Datetime Obj in UTC
Returns:
time {datetime} -- Datetime Obj in LocalTimeZone
"""
# Set original TZ to UTC
time = time.replace(tzinfo=tz.tzutc())
time = time.astimezone(tz.tzlocal())
return time
def get_day(self, time):
"""Converts time obj to Local Day (Mon/Tue)
Arguments:
time {datetime} -- Datetime Obj
Returns:
str -- "Mon, Tue, Wed ..."
"""
newtime = self.convert_local(datetime.fromisoformat(time.split("Z")[0]))
return newtime.strftime("%a")
def get_suntime(self, suntime):
"""Convert sunrise/set to 24 hour
Arguments:
suntime {str} -- String of time in '2020-01-01T00:00:00Z' format
Returns:
str -- Returns HH:MM in 24 format
"""
sun_time = datetime.fromisoformat(suntime.split("Z")[0])
local_time = self.convert_local(sun_time)
return str(local_time.hour)+":"+str(local_time.minute)
def get_weather(self):
"""Get weather and populate lookup dictonary
"""
general_data = self.weather.observations()
#forecast_rain = self.weather.forecast_rain()
forecast_daily = self.weather.forecasts_daily()
try:
self.lookup = {
"temperature": "{}°C".format(general_data["temp"]),
"humidity": "{}%".format(general_data["humidity"]),
"wind": {
"speed": general_data["wind"]["speed_kilometre"],
"direction": general_data["wind"]["direction"]
},
"chance": "{}%".format(forecast_daily[0]["rain"]["chance"]),
"chance_tom": "{}%".format(forecast_daily[1]["rain"]["chance"]),
"uvindex": forecast_daily[0]["uv"],
"uvindex_tom": forecast_daily[1]["uv"],
"sunrise": self.get_suntime(forecast_daily[0]["astronomical"]["sunrise_time"]),
"sunrise_tom": self.get_suntime(forecast_daily[1]["astronomical"]["sunrise_time"]),
"sunset": self.get_suntime(forecast_daily[0]["astronomical"]["sunset_time"]),
"sunset_tom": self.get_suntime(forecast_daily[1]["astronomical"]["sunset_time"]),
"weather_type": forecast_daily[0]["short_text"],
"weather_type_tom": forecast_daily[1]["short_text"],
"weather_code": forecast_daily[0]["icon_descriptor"],
"fire": forecast_daily[0]["fire_danger"],
"fire_tom": forecast_daily[1]["fire_danger"],
"forecast": forecast_daily
}
print(self.lookup["forecast"])
except Exception as e:
print(f"ERROR: {e}")
def get_fact(self):
api = "https://uselessfacts.jsph.pl/random.json?language=en"
r = requests.get(api)
print(r.json()["text"])
return r.json()["text"]
class PiDisplay(PiWeather):
def __init__(self):
PiWeather.__init__(self)
self.display = PapirusComposite(False)
self.unknown_icon = "3200.png"
self.order = []
self.gotWeather = False
self.initalize_order()
self.initalize_display()
def initalize_order(self):
"""Create the order that information is displayed
"""
for stat in self.config["stats"]:
if self.config["stats"][stat]:
self.order.append(stat)
def initalize_display(self):
"""Add all the screen elements to the e-ink display
"""
if self.config["forecast"]["enabled"]:
self.display.AddImg(self.which_icon(self.unknown_icon), 0, 0, (48, 48), Id="WeatherIcon")
self.display.AddText("Loading...", 49, 0, size=17, Id="LineOne",
fontPath='/usr/share/fonts/truetype/freefont/FreeMonoBold.ttf')
self.display.AddText("Loading...", 49, 20, size=15, Id="LineTwo")
self.display.AddText("Loading...", 49, 36, size=15, Id="LineThree")
if self.config["forecast"]["sixday"]:
self.display.AddText("...", 3, 53, size=15, Id="ForecastOne")
self.display.AddText("...", 35, 53, size=15, Id="ForecastTwo")
self.display.AddText(
"...", 68, 53, size=15, Id="ForecastThree")
self.display.AddText(
"...", 101, 53, size=15, Id="ForecastFour")
self.display.AddText(
"...", 135, 53, size=15, Id="ForecastFive")
self.display.AddText("...", 167, 53, size=15, Id="ForecastSix")
self.display.AddImg(self.which_icon(self.unknown_icon), 1, 66, (32, 32), Id="ForecastIconOne")
self.display.AddImg(self.which_icon(self.unknown_icon), 34, 66, (32, 32), Id="ForecastIconTwo")
self.display.AddImg(self.which_icon(self.unknown_icon), 67, 66, (32, 32), Id="ForecastIconThree")
self.display.AddImg(self.which_icon(self.unknown_icon), 100, 66, (32, 32), Id="ForecastIconFour")
self.display.AddImg(self.which_icon(self.unknown_icon), 133, 66, (32, 32), Id="ForecastIconFive")
self.display.AddImg(self.which_icon(self.unknown_icon), 166, 66, (32, 32), Id="ForecastIconSix")
else:
self.display.AddText("Today: ...", 25, 51, size=15, Id="ForecastOne")
self.display.AddText("Tomorrow: ...", 25, 77, size=15, Id="ForecastTwo")
self.display.AddImg(self.which_icon(self.unknown_icon), 1, 53, (23, 23), Id="ForecastIconOne")
self.display.AddImg(self.which_icon(self.unknown_icon), 1, 75, (23, 23), Id="ForecastIconTwo")
else:
self.display.AddImg(self.which_icon(self.unknown_icon), 1, 15, (80, 80), Id="WeatherIcon")
self.display.AddText("Loading...", 1, 1, size=15, Id="LineOne",
fontPath='/usr/share/fonts/truetype/freefont/FreeMonoBold.ttf')
self.display.AddText("Loading...", 82, 16, size=15, Id="LineTwo",
fontPath='/usr/share/fonts/truetype/freefont/FreeMonoBold.ttf')
self.display.AddText("Loading...", 82, 32, size=15, Id="LineThree",
fontPath='/usr/share/fonts/truetype/freefont/FreeMonoBold.ttf')
self.display.AddText("Fact: ", 1, 98, size=14, Id="LineFour")
self.display.AddText("Generating Fact...", 1, 113, size=13, Id="LineFive")
self.display.WriteAll()
def update(self):
"""Regurlarly update the screen with new information
"""
self.gotWeather = False
while not self.gotWeather:
try:
self.get_weather()
self.gotWeather = True
except:
sleep(60)
if not self.lookup:
print("Invalid Location")
exit()
self.display.UpdateImg("WeatherIcon", self.which_icon(str(self.lookup["weather_code"])+".png"))
self.display.UpdateText("LineOne", self.lookup["weather_type"], fontPath='/usr/share/fonts/truetype/freefont/FreeMonoBold.ttf')
self.display.UpdateText("LineFive", "{}".format(self.get_fact()))
if self.config["forecast"]["enabled"]:
if self.config["forecast"]["sixday"]:
self.display.UpdateText(
"ForecastOne", self.get_day(self.lookup["forecast"][1]["date"]))
self.display.UpdateText(
"ForecastTwo", self.get_day(self.lookup["forecast"][2]["date"]))
self.display.UpdateText(
"ForecastThree", self.get_day(self.lookup["forecast"][3]["date"]))
self.display.UpdateText(
"ForecastFour", self.get_day(self.lookup["forecast"][4]["date"]))
self.display.UpdateText(
"ForecastFive", self.get_day(self.lookup["forecast"][5]["date"]))
self.display.UpdateText(
"ForecastSix", self.get_day(self.lookup["forecast"][6]["date"]))
self.display.UpdateImg("ForecastIconOne", self.which_icon(str(self.lookup["forecast"][1]["icon_descriptor"])+'.png'))
self.display.UpdateImg("ForecastIconTwo", self.which_icon(str(self.lookup["forecast"][2]["icon_descriptor"])+'.png'))
self.display.UpdateImg("ForecastIconThree", self.which_icon(str(self.lookup["forecast"][3]["icon_descriptor"])+'.png'))
self.display.UpdateImg("ForecastIconFour", self.which_icon(str(self.lookup["forecast"][4]["icon_descriptor"])+'.png'))
self.display.UpdateImg("ForecastIconFive", self.which_icon(str(self.lookup["forecast"][5]["icon_descriptor"])+'.png'))
self.display.UpdateImg("ForecastIconSix", self.which_icon(str(self.lookup["forecast"][6]["icon_descriptor"])+'.png'))
else:
if self.lookup["forecast"][0]["temp_min"] is None:
self.display.UpdateText(
"ForecastOne", self.get_day(self.lookup["forecast"][1]["date"])+": "+self.lookup["forecast"][1]["short_text"])
self.display.UpdateText(
"ForecastTwo", self.get_day(self.lookup["forecast"][2]["date"])+": "+self.lookup["forecast"][2]["short_text"])
self.display.UpdateImg("ForecastIconOne", self.which_icon(str(self.lookup["forecast"][1]["icon_descriptor"])+'.png'))
self.display.UpdateImg("ForecastIconTwo", self.which_icon(str(self.lookup["forecast"][2]["icon_descriptor"])+'.png'))
else:
self.display.UpdateText(
"ForecastOne", "Today: "+self.lookup["forecast"][0]["short_text"])
self.display.UpdateText(
"ForecastTwo", "Tomorrow: "+self.lookup["forecast"][1]["short_text"])
self.display.UpdateImg("ForecastIconOne", self.which_icon(str(self.lookup["forecast"][0]["icon_descriptor"])+'.png'))
self.display.UpdateImg("ForecastIconTwo", self.which_icon(str(self.lookup["forecast"][1]["icon_descriptor"])+'.png'))
for stat in self.order:
# Bom sets "temp_min" to null when its near night-time so use that and get the tonight/tomorrow information
# "Tomorrow"
if self.lookup["forecast"][0]["temp_min"] is None:
if stat == "temperature":
self.display.UpdateText("LineTwo", "Night Min: "+str(self.lookup["forecast"][0]["now"]["temp_now"]),
fontPath='/usr/share/fonts/truetype/freefont/FreeMonoBold.ttf')
self.display.UpdateText("LineThree", "Tomorrow Max: "+str(self.lookup["forecast"][0]["now"]["temp_later"]),
fontPath='/usr/share/fonts/truetype/freefont/FreeMonoBold.ttf')
elif stat == "chance":
if str(self.lookup["chance_tom"]) == "None":
rain_chance = '0%'
else:
rain_chance = "{}".format(self.lookup["chance_tom"])
max_rain = "{}{}".format(
self.lookup["forecast"][1]["rain"]["amount"]["max"],
self.lookup["forecast"][1]["rain"]["amount"]["units"])
self.display.UpdateText("LineTwo", "Tommorow Rain:",
fontPath='/usr/share/fonts/truetype/freefont/FreeMonoBold.ttf')
self.display.UpdateText(
"LineThree",
"{} with {}".format(rain_chance, max_rain),
fontPath='/usr/share/fonts/truetype/freefont/FreeMonoBold.ttf')
self.display.UpdateText(
"LineFour",
"Take an Umbrella Tomorrow!",
fontPath='/usr/share/fonts/truetype/freefont/FreeMonoBold.ttf')
elif stat == "sun":
self.display.UpdateText("LineTwo", "Sunrise Tomorrow: {}".format(self.lookup["sunrise_tom"]))
self.display.UpdateText("LineThree", "Sunset Tomorrow: {}".format(self.lookup["sunset_tom"]))
elif stat == "uv_fire":
self.display.UpdateText("LineTwo", "UV Tomorrow: "+self.lookup["uvindex_tom"]["category"])
self.display.UpdateText("LineThree", "Fire Danger Tom: "+self.lookup["fire_tom"])
if self.lookup["uvindex_tom"]["max_index"] > 16:
self.display.UpdateText("LineFour", "!!! You NEED Sunscreen !!!",
fontPath='/usr/share/fonts/truetype/freefont/FreeMonoBold.ttf')
elif stat == "humid_wind":
# Can't get tomorrow's humidity... show now... no-one will notice
humidity = int(self.lookup["humidity"][:-1])
scale = ""
if humidity < 5:
scale = "Arid Desert"
elif humidity < 25:
scale = "Dry heat"
elif humidity < 60:
scale = "Average Humid"
elif humidity < 80:
scale = "H: Pretty humid"
else:
scale = "HUMID AF"
self.display.UpdateText(
"LineTwo", "{}: {}".format(scale, self.lookup["humidity"]),
fontPath='/usr/share/fonts/truetype/freefont/FreeMonoBold.ttf')
self.display.UpdateText(
"LineThree", "Speed: {} | Dir: {}".format(self.lookup["wind"]["speed"], self.lookup["wind"]["direction"]))
else:
# Its Morning we care about Today
if stat == "temperature":
self.display.UpdateText("LineTwo", "Now Temp: "+self.lookup[stat],
fontPath='/usr/share/fonts/truetype/freefont/FreeMonoBold.ttf')
self.display.UpdateText(
"LineThree", "Low: {} -> High: {}".format(self.lookup["forecast"][0]["temp_min"], self.lookup["forecast"][0]["temp_max"]),
fontPath='/usr/share/fonts/truetype/freefont/FreeMonoBold.ttf')
elif stat == "chance":
if self.lookup[stat] == "None%":
rain_chance = '0%'
else:
rain_chance = self.lookup[stat]
self.display.UpdateText("LineTwo", "Rain Chance",
fontPath='/usr/share/fonts/truetype/freefont/FreeMonoBold.ttf')
self.display.UpdateText("LineThree", rain_chance,
fontPath='/usr/share/fonts/truetype/freefont/FreeMonoBold.ttf')
if int(rain_chance[:-1]) > 40:
self.display.UpdateText("LineFive", "Umbrealla Today")
elif stat == "humid_wind":
humidity = int(self.lookup[stat][:-1])
scale = ""
if humidity < 5:
scale = "Arid Desert"
elif humidity < 25:
scale = "Dry heat"
elif humidity < 60:
scale = "Average Humid"
elif humidity < 80:
scale = "H: Pretty humid"
else:
scale = "HUMID AF"
self.display.UpdateText(
"LineTwo", "{}: "+self.lookup[stat],
fontPath='/usr/share/fonts/truetype/freefont/FreeMonoBold.ttf')
self.display.UpdateText(
"LineThree", "Speed: {} | Dir: {}".format(self.lookup[stat]["speed"], self.lookup[stat]["direction"]))
elif stat == "uv_fire":
self.display.UpdateText("LineTwo", "UV Index: "+self.lookup["uvindex"]["category"])
self.display.UpdateText("LineThree", "Fire Danger: "+self.lookup["fire"])
if self.lookup["uvindex"]["max_index"] > 16:
self.display.UpdateText("LineFour", "!!! You NEED Sunscreen !!!",
fontPath='/usr/share/fonts/truetype/freefont/FreeMonoBold.ttf')
elif stat == "sun":
self.display.UpdateText("LineTwo", "Sunrise: {}".format(self.lookup["sunrise"]))
self.display.UpdateText("LineThree", "Sunset: {}".format(self.lookup["sunset"]))
self.display.WriteAll(partialUpdate=True)
if len(self.order) >= 3:
sleep(20)
else:
sleep(int(60/len(self.order)))
# Can only request weather data every 43 seconds (2000 calls a day)
# 20 seconds per slide is safe
def which_icon(self, icon_code):
"""If the icon does not exist, it defaults to Unknown
Argument: codename
"""
try:
if os.path.isfile(os.path.join(DIRECTORY, 'images', 'weather', str(icon_code))):
return os.path.join(DIRECTORY, 'images', 'weather', str(icon_code))
else:
print("INFO: {} not exist".format(icon_code))
return os.path.join(DIRECTORY, 'images', 'weather', str(self.unknown_icon))
except Exception as e:
print(e)
return os.path.join(DIRECTORY, 'images', 'weather', str(self.unknown_icon))
PI = PiDisplay()
if __name__ == "__main__":
while True:
PI.update()