From: sandyjmacdonald Date: Fri, 8 Nov 2019 20:26:45 +0000 (+0000) Subject: Adding all in one weather and light display example X-Git-Url: https://zdv2.bktei.com/gitweb/EVA-2020-02-2.git/commitdiff_plain/2fe226f8a1755e1cf487c972b56b9166b4f2ff4b?ds=inline;hp=--cc Adding all in one weather and light display example --- 2fe226f8a1755e1cf487c972b56b9166b4f2ff4b diff --git a/examples/icons/bulb-bright.png b/examples/icons/bulb-bright.png new file mode 100644 index 0000000..5697a81 Binary files /dev/null and b/examples/icons/bulb-bright.png differ diff --git a/examples/icons/bulb-dark.png b/examples/icons/bulb-dark.png new file mode 100644 index 0000000..a91e24b Binary files /dev/null and b/examples/icons/bulb-dark.png differ diff --git a/examples/icons/bulb-dim.png b/examples/icons/bulb-dim.png new file mode 100644 index 0000000..a91e24b Binary files /dev/null and b/examples/icons/bulb-dim.png differ diff --git a/examples/icons/bulb-light.png b/examples/icons/bulb-light.png new file mode 100644 index 0000000..f8fb791 Binary files /dev/null and b/examples/icons/bulb-light.png differ diff --git a/examples/icons/humidity-bad.png b/examples/icons/humidity-bad.png new file mode 100644 index 0000000..5a7201c Binary files /dev/null and b/examples/icons/humidity-bad.png differ diff --git a/examples/icons/humidity-good.png b/examples/icons/humidity-good.png new file mode 100644 index 0000000..ba450db Binary files /dev/null and b/examples/icons/humidity-good.png differ diff --git a/examples/icons/humidity.png b/examples/icons/humidity.png new file mode 100644 index 0000000..5a7201c Binary files /dev/null and b/examples/icons/humidity.png differ diff --git a/examples/icons/temperature.png b/examples/icons/temperature.png new file mode 100644 index 0000000..54a826f Binary files /dev/null and b/examples/icons/temperature.png differ diff --git a/examples/icons/weather-change.png b/examples/icons/weather-change.png new file mode 100644 index 0000000..21215b7 Binary files /dev/null and b/examples/icons/weather-change.png differ diff --git a/examples/icons/weather-dry.png b/examples/icons/weather-dry.png new file mode 100644 index 0000000..2302926 Binary files /dev/null and b/examples/icons/weather-dry.png differ diff --git a/examples/icons/weather-fair.png b/examples/icons/weather-fair.png new file mode 100644 index 0000000..2302926 Binary files /dev/null and b/examples/icons/weather-fair.png differ diff --git a/examples/icons/weather-rain.png b/examples/icons/weather-rain.png new file mode 100644 index 0000000..a7dea2f Binary files /dev/null and b/examples/icons/weather-rain.png differ diff --git a/examples/icons/weather-storm.png b/examples/icons/weather-storm.png new file mode 100644 index 0000000..2017245 Binary files /dev/null and b/examples/icons/weather-storm.png differ diff --git a/examples/weather-and-light.py b/examples/weather-and-light.py new file mode 100644 index 0000000..a0d755d --- /dev/null +++ b/examples/weather-and-light.py @@ -0,0 +1,409 @@ +import time +import numpy +import colorsys +from PIL import Image, ImageDraw, ImageFont, ImageFilter +from fonts.ttf import RobotoMedium as UserFont + +import ST7735 +from bme280 import BME280 +from ltr559 import LTR559 + +import pytz +from astral import Astral +from datetime import datetime, timedelta + +try: + from smbus2 import SMBus +except ImportError: + from smbus import SMBus + + +def calculate_y_pos(x, centre): + """Calculates the y-coordinate on a parabolic curve, given x.""" + centre = 80 + y = 1 / centre * (x - centre) ** 2 + + return int(y) + + +def circle_coordinates(x, y, radius): + """Calculates the bounds of a circle, given centre and radius.""" + + x1 = x - radius # Left + x2 = x + radius # Right + y1 = y - radius # Bottom + y2 = y + radius # Top + + return (x1, y1, x2, y2) + + +def map_colour(x, centre, start_hue, end_hue, day): + """Given an x coordinate and a centre point, a start and end hue (in degrees), + and a Boolean for day or night (day is True, night False), calculate a colour + hue representing the 'colour' of that time of day.""" + + start_hue = start_hue / 360 # Rescale to between 0 and 1 + end_hue = end_hue / 360 + + sat = 1.0 + + # Dim the brightness as you move from the centre to the edges + val = 1 - (abs(centre - x) / (2 * centre)) + + # Ramp up towards centre, then back down + if x > centre: + x = (2 * centre) - x + + # Calculate the hue + hue = start_hue + ((x / centre) * (end_hue - start_hue)) + + # At night, move towards purple/blue hues and reverse dimming + if not day: + hue = 1 - hue + val = 1 - val + + r, g, b = [int(c * 255) for c in colorsys.hsv_to_rgb(hue, sat, val)] + + return (r, g, b) + + +def x_from_sun_moon_time(progress, period, x_range): + """Recalculate/rescale an amount of progress through a time period.""" + + x = int((progress / period) * x_range) + + return x + + +def sun_moon_time(dt, city_name, time_zone): + """Calculate the progress through the current sun/moon period (i.e day or + night) from the last sunrise or sunset, given a datetime object 't'.""" + + a = Astral() + city = a[city_name] + + # Datetime objects for yesterday, today, tomorrow + today = dt.date() + dt = pytz.timezone(time_zone).localize(dt) + yesterday = today - timedelta(1) + tomorrow = today + timedelta(1) + + # Sun objects for yesterfay, today, tomorrow + sun_yesterday = city.sun(date=yesterday, local=True) + sun = city.sun(date=today, local=True) + sun_tomorrow = city.sun(date=tomorrow, local=True) + + # Work out sunset yesterday, sunrise/sunset today, and sunrise tomorrow + sunset_yesterday = sun_yesterday["sunset"] + sunrise_today = sun["sunrise"] + sunset_today = sun["sunset"] + sunrise_tomorrow = sun_tomorrow["sunrise"] + + # Work out lengths of day or night period and progress through period + if sunrise_today < dt < sunset_today: + day = True + period = sunset_today - sunrise_today + mid = sunrise_today + (period / 2) + progress = dt - sunrise_today + + elif dt > sunset_today: + day = False + period = sunrise_tomorrow - sunset_today + mid = sunset_today + (period / 2) + progress = dt - sunset_today + + else: + day = False + period = sunrise_today - sunset_yesterday + mid = sunset_yesterday + (period / 2) + progress = dt - sunset_yesterday + + # Convert time deltas to seconds + progress = progress.total_seconds() + period = period.total_seconds() + + return (progress, period, day) + + +def draw_background(progress, period, day): + """Given an amount of progress through the day or night, draw the + background colour and overlay a blurred sun/moon.""" + + # x-coordinate for sun/moon + x = x_from_sun_moon_time(progress, period, WIDTH) + + # If it's day, then move right to left + if day: + x = WIDTH - x + + # Calculate position on sun/moon's curve + centre = WIDTH / 2 + y = calculate_y_pos(x, centre) + + # Background colour + background = map_colour(x, 80, mid_hue, day_hue, day) + + # New image for background colour + img = Image.new('RGBA', (WIDTH, HEIGHT), color=background) + draw = ImageDraw.Draw(img) + + # New image for sun/moon overlay + overlay = Image.new('RGBA', (WIDTH, HEIGHT), color=(0, 0, 0, 0)) + overlay_draw = ImageDraw.Draw(overlay) + + # Draw the sun/moon + circle = circle_coordinates(x, y, sun_radius) + overlay_draw.ellipse(circle, fill=(200, 200, 50, opacity)) + + # Overlay the sun/moon on the background as an alpha matte + composite = Image.alpha_composite(img, overlay).filter(ImageFilter.GaussianBlur(radius=blur)) + + return composite + + +def overlay_text(img, position, text, font, align_right=False, rectangle=False): + draw = ImageDraw.Draw(img) + w, h = font.getsize(text) + if align_right: + x, y = position + x -= w + position = (x, y) + if rectangle: + x += 1 + y += 1 + position = (x, y) + border = 1 + rect = (x - border, y, x + w, y + h + border) + rect_img = Image.new('RGBA', (WIDTH, HEIGHT), color=(0, 0, 0, 0)) + rect_draw = ImageDraw.Draw(rect_img) + rect_draw.rectangle(rect, (255, 255, 255)) + rect_draw.text(position, text, font=font, fill=(0, 0, 0, 0)) + img = Image.alpha_composite(img, rect_img) + else: + draw.text(position, text, font=font, fill=(255, 255, 255)) + return img + + +def get_cpu_temperature(): + with open("/sys/class/thermal/thermal_zone0/temp", "r") as f: + temp = f.read() + temp = int(temp) / 1000.0 + return temp + + +def correct_humidity(humidity, temperature, corr_temperature): + dewpoint = temperature - ((100 - humidity) / 5) + corr_humidity = 100 - (5 * (corr_temperature - dewpoint)) + return min(100, corr_humidity) + + +def analyse_pressure(pressure, t): + global time_vals, pressure_vals, trend + if len(pressure_vals) > num_vals: + pressure_vals = pressure_vals[1:] + [pressure] + time_vals = time_vals[1:] + [t] + + # Calculate line of best fit + line = numpy.polyfit(time_vals, pressure_vals, 1, full=True) + + # Calculate slope, variance, and confidence + slope = line[0][0] + intercept = line[0][1] + variance = numpy.var(pressure_vals) + residuals = numpy.var([(slope * x + intercept - y) for x, y in zip(time_vals, pressure_vals)]) + r_squared = 1 - residuals / variance + + # Calculate change in pressure per hour + change_per_hour = slope * 60 * 60 + variance_per_hour = variance * 60 * 60 + + mean_pressure = numpy.mean(pressure_vals) + + # Calculate trend + if r_squared > 0.5: + if change_per_hour > 0.5: + trend = ">" + elif change_per_hour < -0.5: + trend = "<" + elif -0.5 <= change_per_hour <= 0.5: + trend = "-" + + if trend != "-": + if abs(change_per_hour) > 3: + trend *= 2 + else: + pressure_vals.append(pressure) + time_vals.append(t) + mean_pressure = numpy.mean(pressure_vals) + change_per_hour = 0 + trend = "-" + +# time.sleep(interval) + + return (mean_pressure, change_per_hour, trend) + +def describe_pressure(pressure): + """Convert pressure into barometer-type description.""" + if pressure < 970: + description = "storm" + elif 970 <= pressure < 990: + description = "rain" + elif 990 <= pressure < 1010: + description = "change" + elif 1010 <= pressure < 1030: + description = "fair" + elif pressure >= 1030: + description = "dry" + else: + description = "" + return description + + +def describe_humidity(humidity): + """Convert relative humidity into good/bad description.""" + if 40 < humidity < 60: + description = "good" + else: + description = "bad" + return description + + +def describe_light(light): + """Convert light level in lux to descriptive value.""" + if light < 50: + description = "dark" + elif 50 <= light < 100: + description = "dim" + elif 100 <= light < 500: + description = "light" + elif light >= 500: + description = "bright" + return description + + +# Initialise the LCD +disp = ST7735.ST7735( + port=0, + cs=1, + dc=9, + backlight=12, + rotation=270, + spi_speed_hz=10000000 +) + +disp.begin() + +WIDTH = disp.width +HEIGHT = disp.height + +# The city and timezone that you want to display. +city_name = "Sheffield" +time_zone = "Europe/London" + +# Values that alter the look of the background +blur = 50 +opacity = 125 + +mid_hue = 0 +day_hue = 25 + +sun_radius = 50 + +# Fonts +font_sm = ImageFont.truetype(UserFont, 12) +font_lg = ImageFont.truetype(UserFont, 14) + +# Margins +margin = 3 + +dt = datetime.now() + +# Set up BME280 weather sensor +bus = SMBus(1) +bme280 = BME280(i2c_dev=bus) + +min_temp = bme280.get_temperature() +max_temp = bme280.get_temperature() + +factor = 2.25 +cpu_temps = [get_cpu_temperature()] * 5 + +# Set up light sensor +ltr559 = LTR559() + +# Pressure variables +pressure_vals = [] +time_vals = [] +num_vals = 1000 +interval = 1 +trend = "-" + +while True: + dt = datetime.now() +# dt += timedelta(minutes=5) + progress, period, day = sun_moon_time(dt, city_name, time_zone) + background = draw_background(progress, period, day) + + # Time. + date_string = dt.strftime("%d %b %y").lstrip('0') + time_string = dt.strftime("%H:%M") + img = overlay_text(background, (0 + margin, 0 + margin), time_string, font_lg) + img = overlay_text(img, (WIDTH - margin, 0 + margin), date_string, font_lg, align_right=True) + + # Temperature + temperature = bme280.get_temperature() + + # Corrected temperature + cpu_temp = get_cpu_temperature() + cpu_temps = cpu_temps[1:] + [cpu_temp] + avg_cpu_temp = sum(cpu_temps) / float(len(cpu_temps)) + corr_temperature = temperature - ((avg_cpu_temp - temperature) / factor) + + if corr_temperature < min_temp: + min_temp = corr_temperature + elif corr_temperature > max_temp: + max_temp = corr_temperature + + temp_string = f"{corr_temperature:.0f}°C" + img = overlay_text(img, (68, 18), temp_string, font_lg, align_right=True) + spacing = font_lg.getsize(temp_string)[1] + 1 + range_string = f"{min_temp:.0f}-{max_temp:.0f}" + img = overlay_text(img, (68, 18 + spacing), range_string, font_sm, align_right=True, rectangle=True) + temp_icon = Image.open("icons/temperature.png") + img.paste(temp_icon, (margin, 18), mask=temp_icon) + + # Humidity + humidity = bme280.get_humidity() + corr_humidity = correct_humidity(humidity, temperature, corr_temperature) + humidity_string = f"{corr_humidity:.0f}%" + img = overlay_text(img, (68, 48), humidity_string, font_lg, align_right=True) + spacing = font_lg.getsize(humidity_string)[1] + 1 + humidity_desc = describe_humidity(corr_humidity).upper() + img = overlay_text(img, (68, 48 + spacing), humidity_desc, font_sm, align_right=True, rectangle=True) + humidity_icon = Image.open("icons/humidity-" + humidity_desc.lower() + ".png") + img.paste(humidity_icon, (margin, 48), mask=humidity_icon) + + # Light + light = ltr559.get_lux() + light_string = f"{int(light):,}" + img = overlay_text(img, (WIDTH - margin, 18), light_string, font_lg, align_right=True) + spacing = font_lg.getsize(light_string.replace(",", ""))[1] + 1 + light_desc = describe_light(light).upper() + img = overlay_text(img, (WIDTH - margin - 1, 18 + spacing), light_desc, font_sm, align_right=True, rectangle=True) + light_icon = Image.open("icons/bulb-" + light_desc.lower() + ".png") + img.paste(humidity_icon, (80, 18), mask=light_icon) + + # Pressure + pressure = bme280.get_pressure() + t = time.time() + mean_pressure, change_per_hour, trend = analyse_pressure(pressure, t) + pressure_string = f"{int(mean_pressure):,} {trend}" + img = overlay_text(img, (WIDTH - margin, 48), pressure_string, font_lg, align_right=True) + pressure_desc = describe_pressure(mean_pressure).upper() + spacing = font_lg.getsize(pressure_string.replace(",", ""))[1] + 1 + img = overlay_text(img, (WIDTH - margin - 1, 48 + spacing), pressure_desc, font_sm, align_right=True, rectangle=True) + pressure_icon = Image.open("icons/weather-" + pressure_desc.lower() + ".png") + img.paste(pressure_icon, (80, 48), mask=pressure_icon) + + # Display image + disp.display(img)