Files
drunkendotfiles/vendor/breakout-garden/examples/weather/weather.py
dissimulo 030172f523 Initial backup of LTP-305G matrix clock setup on matrixpi
Captures everything needed to redeploy the two-display clock (hour on I2C
0x61, minute on I2C 0x63) on a fresh Pi:

- Both systemd units (matrix0x61.service, matrix0x63.service)
- Deployed Pimoroni script tree, including the local %I (12-hour) clock
  customization
- Vendored upstream sources (ltp305-python, breakout-garden) so restore is
  fully offline-capable
- Boot config snippet enabling I2C
- install.sh that wires it all back up idempotently
- Inventory doc cross-referencing every live-system path

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 01:32:39 -07:00

219 lines
6.6 KiB
Python
Executable File

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import time
import datetime
import glob
import logging
import sys
try:
import requests
import geocoder
import lxml
from bs4 import BeautifulSoup
from PIL import Image
from PIL import ImageFont
from PIL import ImageDraw
except ImportError:
print("""
This script requires several modules to run correctly.
Install with:
sudo pip install requests geocoder beautifulsoup4
sudo apt install python{v}-lxml python{v}-pil
""".format(v="" if sys.version_info.major == 2 else sys.version_info.major))
sys.exit(1)
import bme680
from luma.core.interface.serial import spi
from luma.core.error import DeviceNotFoundError
from luma.oled.device import sh1106
TEMPERATURE_UPDATE_INTERVAL = 0.1 # in seconds
# Default to Sheffield-on-Sea for location
CITY = "Sheffield"
COUNTRYCODE = "GB"
# Used to calibrate the sensor
TEMP_OFFSET = 0.0
logging.basicConfig(level=os.environ.get("LOGLEVEL", "WARNING"))
print("""This Pimoroni Breakout Garden example requires a
BME680 Environmental Sensor Breakout and a 1.12" OLED Breakout.
This example turns your Breakout Garden into a mini weather display
combining indoor temperature and pressure data with a weather icon
indicating the current local weather conditions.
Press Ctrl+C a couple times to exit.
""")
# Convert a city name and country code to latitude and longitude
def get_coords(address):
g = geocoder.arcgis(address)
coords = g.latlng
logging.info("Location coordinates: %s", coords)
return coords
# Query Dark Sky (https://darksky.net/) to scrape current weather data
def get_weather(coords):
weather = {}
try:
res = requests.get("https://darksky.net/forecast/{}/uk212/en".format(","
.join([str(c) for c in coords])))
if res.status_code == 200:
soup = BeautifulSoup(res.content, "lxml")
curr = soup.find("span", "currently")
if curr:
img_name = curr.img["alt"].split()[0]
logging.info("Weather summary: %s", img_name)
weather["summary"] = img_name
except requests.exceptions.RequestException as e:
logging.error("Could not get weather data from DarkSky: {}".format(e))
pass
return weather
# This maps the weather summary from Dark Sky
# to the appropriate weather icons
icon_map = {
"snow": ["snow", "sleet"],
"rain": ["rain"],
"cloud": ["fog", "cloudy", "partly-cloudy-day", "partly-cloudy-night"],
"sun": ["clear-day", "clear-night"],
"storm": [],
"wind": ["wind"]
}
# Pre-load icons into a dictionary with PIL
icons = {}
for icon in glob.glob("icons/*.png"):
icon_name = icon.split("/")[1].replace(".png", "")
icon_image = Image.open(icon)
icons[icon_name] = icon_image
location_string = "{city}, {countrycode}".format(city=CITY,
countrycode=COUNTRYCODE)
coords = get_coords(location_string)
def get_weather_icon(weather):
if weather:
summary = weather["summary"]
for icon in icon_map:
if summary in icon_map[icon]:
logging.info("Weather icon: %s", icon)
return icons[icon]
logging.error("Could not determine icon for weather")
return None
else:
logging.error("No weather information provided to get icon")
return None
# Get initial weather data for the given location
weather_icon = get_weather_icon(get_weather(coords))
# Set up OLED
oled = sh1106(spi(port=0, device=1, gpio_DC=9), rotate=2, height=128, width=128)
# Set up BME680 sensor
sensor = bme680.BME680()
sensor.set_humidity_oversample(bme680.OS_2X)
sensor.set_pressure_oversample(bme680.OS_4X)
sensor.set_temperature_oversample(bme680.OS_8X)
sensor.set_filter(bme680.FILTER_SIZE_3)
sensor.set_temp_offset(TEMP_OFFSET)
# Load fonts
rr_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'fonts',
'Roboto-Regular.ttf'))
rb_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'fonts',
'Roboto-Black.ttf'))
rr_24 = ImageFont.truetype(rr_path, 24)
rb_20 = ImageFont.truetype(rb_path, 20)
rr_12 = ImageFont.truetype(rr_path, 12)
# Fetch sensor dating first so that device settings take effect
sensor.get_sensor_data()
# Initial values
low_temp = sensor.data.temperature
high_temp = sensor.data.temperature
curr_date = datetime.date.today().day
last_checked = time.time()
# Main loop
while True:
# Limit calls to Dark Sky to 1 per minute
if time.time() - last_checked > 60:
weather_icon = get_weather_icon(get_weather(coords))
last_checked = time.time()
# Load in the background image
background = Image.open("images/weather.png").convert(oled.mode)
# Place the weather icon and draw the background
if weather_icon:
background.paste(weather_icon, (10, 46))
draw = ImageDraw.ImageDraw(background)
# Gets temp. and press. and keeps track of daily min and max temp
if sensor.get_sensor_data():
temp = sensor.data.temperature
press = sensor.data.pressure
if datetime.datetime.today().day == curr_date:
if temp < low_temp:
low_temp = temp
elif temp > high_temp:
high_temp = temp
else:
curr_date = datetime.datetime.today().day
low_temp = temp
high_temp = temp
# Write temp. and press. to image
draw.text((8, 22), "{0:4.0f}".format(press),
fill="white", font=rb_20)
draw.text((86, 12), u"{0:2.0f}°".format(temp),
fill="white", font=rb_20)
# Write min and max temp. to image
draw.text((80, 0), u"max: {0:2.0f}°".format(high_temp),
fill="white", font=rr_12)
draw.text((80, 110), u"min: {0:2.0f}°".format(low_temp),
fill="white", font=rr_12)
# Write the 24h time and blink the separator every second
if int(time.time()) % 2 == 0:
draw.text((4, 98), datetime.datetime.now().strftime("%H:%M"),
fill="white", font=rr_24)
else:
draw.text((4, 98), datetime.datetime.now().strftime("%H %M"),
fill="white", font=rr_24)
# These lines display the temp. on the thermometer image
draw.rectangle([(97, 43), (100, 86)], fill="black")
temp_offset = 86 - ((86 - 43) * ((temp - 20) / (32 - 20)))
draw.rectangle([(97, temp_offset), (100, 86)], fill="white")
# Display the completed image on the OLED
oled.display(background)
time.sleep(TEMPERATURE_UPDATE_INTERVAL)