Working PoC
@ -0,0 +1,49 @@
|
||||
# Pi-Weather
|
||||
|
||||
Weather Station built for Raspberry Pi Zero with a 2.7" PaPiRus E-Ink Display
|
||||
|
||||
## How to install
|
||||
|
||||
**1. First install requirements:**
|
||||
|
||||
```bash
|
||||
#PaPiRus
|
||||
curl -sSL https://pisupp.ly/papiruscode | sudo bash
|
||||
|
||||
#Weather API
|
||||
sudo python3 -m pip install weather_au
|
||||
```
|
||||
|
||||
More info on **PaPiRus** manual setup can be found on their [repository](https://www.github.com/PiSupply/PaPiRus).
|
||||
|
||||
**2. Modify config.json with your location**
|
||||
```bash
|
||||
$EDITOR config.json
|
||||
```
|
||||
|
||||
**3. Run the python script**
|
||||
|
||||
```bash
|
||||
python3 pi_weather.py
|
||||
```
|
||||
|
||||
**4. You should get a result similar to this:**
|
||||
|
||||

|
||||
|
||||
|
||||
**5. Install the systemd service to start on boot**
|
||||
|
||||
```bash
|
||||
sudo cp piweather.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable pipweather.service
|
||||
```
|
||||
## Features
|
||||
|
||||
- Uses BOM API / Scraping (via weather_au pip module)
|
||||
- Grabs a random fact each rotation
|
||||
|
||||
## Future Work
|
||||
|
||||
Replace PaPirusComposite with something else, so that we can Update specific sections when items change
|
@ -0,0 +1,19 @@
|
||||
{
|
||||
"weather": {
|
||||
"unit": "C",
|
||||
"location": "Melbourne",
|
||||
"wind_direction": "compass",
|
||||
"stats": {
|
||||
"temperature": true,
|
||||
"chance": true,
|
||||
"uvindex": true,
|
||||
"sun": true,
|
||||
"humidity": true,
|
||||
"wind": true
|
||||
},
|
||||
"forecast": {
|
||||
"enabled": true,
|
||||
"sixday": true
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 109 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.8 KiB |
@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=Pi Weather via Display
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/env python3 /home/pi/Documents/Pi-Weather/pi_weather.py
|
||||
Type=simple
|
||||
Restart=always
|
||||
RestartSec=10s
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.3 KiB |
@ -0,0 +1,361 @@
|
||||
#!/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_rain['chance']),
|
||||
"uvindex": forecast_daily[0]["uv"]["category"],
|
||||
"sunrise": self.get_suntime(forecast_daily[0]["astronomical"]["sunrise_time"]),
|
||||
"sunset": self.get_suntime(forecast_daily[0]["astronomical"]["sunset_time"]),
|
||||
"weather_type": forecast_daily[0]["short_text"],
|
||||
"weather_code": forecast_daily[0]["icon_descriptor"],
|
||||
"forecast": forecast_daily
|
||||
}
|
||||
except Exception as e:
|
||||
print("ERROR: {}".format(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,
|
||||
74, 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 Exception:
|
||||
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:
|
||||
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"][1]["icon_descriptor"])+'.png'))
|
||||
self.display.UpdateImg("ForecastIconTwo", self.which_icon(str(self.lookup["forecast"][2]["icon_descriptor"])+'.png'))
|
||||
|
||||
for stat in self.order:
|
||||
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 == "humidity":
|
||||
self.display.UpdateText(
|
||||
"LineTwo", "Humidity: "+self.lookup[stat],
|
||||
fontPath='/usr/share/fonts/truetype/freefont/FreeMonoBold.ttf')
|
||||
humidity = int(self.lookup[stat][:-1])
|
||||
scale = ""
|
||||
if humidity < 5:
|
||||
scale = "Arid Desert"
|
||||
elif humidity < 25:
|
||||
scale = "It\'s a dry heat"
|
||||
elif humidity < 60:
|
||||
scale = "Dry"
|
||||
elif humidity < 80:
|
||||
scale = "It\'s pretty humid"
|
||||
else:
|
||||
scale = "HUMID AF"
|
||||
self.display.UpdateText("LineThree", scale,
|
||||
fontPath='/usr/share/fonts/truetype/freefont/FreeMonoBold.ttf')
|
||||
elif stat == "wind":
|
||||
self.display.UpdateText(
|
||||
"LineTwo", "Speed: {}".format(self.lookup[stat]["speed"]))
|
||||
self.display.UpdateText(
|
||||
"LineThree", "Direction: {}".format(self.lookup[stat]["direction"]))
|
||||
elif stat == "uvindex":
|
||||
self.display.UpdateText("LineTwo", "UV Index")
|
||||
self.display.UpdateText("LineThree", self.lookup[stat])
|
||||
elif stat == "sun":
|
||||
self.display.UpdateText("LineTwo", "Sunrise || Sunset")
|
||||
self.display.UpdateText("LineThree", "{} || {}".format(self.lookup["sunrise"], self.lookup["sunset"]))
|
||||
|
||||
self.display.WriteAll()
|
||||
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
|
||||
|
||||
Return: str for image
|
||||
"""
|
||||
|
||||
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()
|