Prep for v0.0.2
[EVA-2020-02-2.git] / examples / weather-and-light.py
CommitLineData
df20089d
SM
1#!/usr/bin/env python3
2
20442c9a 3import os
2fe226f8 4import time
5import numpy
6import colorsys
7from PIL import Image, ImageDraw, ImageFont, ImageFilter
8from fonts.ttf import RobotoMedium as UserFont
9
10import ST7735
11from bme280 import BME280
12from ltr559 import LTR559
13
14import pytz
20442c9a 15from astral.geocoder import database, lookup
16from astral.sun import sun
2fe226f8 17from datetime import datetime, timedelta
18
19try:
20 from smbus2 import SMBus
21except ImportError:
22 from smbus import SMBus
23
24
25def calculate_y_pos(x, centre):
26 """Calculates the y-coordinate on a parabolic curve, given x."""
27 centre = 80
28 y = 1 / centre * (x - centre) ** 2
29
30 return int(y)
31
32
33def circle_coordinates(x, y, radius):
34 """Calculates the bounds of a circle, given centre and radius."""
35
36 x1 = x - radius # Left
37 x2 = x + radius # Right
38 y1 = y - radius # Bottom
39 y2 = y + radius # Top
40
41 return (x1, y1, x2, y2)
42
43
44def map_colour(x, centre, start_hue, end_hue, day):
45 """Given an x coordinate and a centre point, a start and end hue (in degrees),
46 and a Boolean for day or night (day is True, night False), calculate a colour
47 hue representing the 'colour' of that time of day."""
48
49 start_hue = start_hue / 360 # Rescale to between 0 and 1
50 end_hue = end_hue / 360
51
52 sat = 1.0
53
54 # Dim the brightness as you move from the centre to the edges
55 val = 1 - (abs(centre - x) / (2 * centre))
56
57 # Ramp up towards centre, then back down
58 if x > centre:
59 x = (2 * centre) - x
60
61 # Calculate the hue
62 hue = start_hue + ((x / centre) * (end_hue - start_hue))
63
64 # At night, move towards purple/blue hues and reverse dimming
65 if not day:
66 hue = 1 - hue
67 val = 1 - val
68
69 r, g, b = [int(c * 255) for c in colorsys.hsv_to_rgb(hue, sat, val)]
70
71 return (r, g, b)
72
73
74def x_from_sun_moon_time(progress, period, x_range):
75 """Recalculate/rescale an amount of progress through a time period."""
76
77 x = int((progress / period) * x_range)
78
79 return x
80
81
82def sun_moon_time(dt, city_name, time_zone):
83 """Calculate the progress through the current sun/moon period (i.e day or
84 night) from the last sunrise or sunset, given a datetime object 't'."""
85
20442c9a 86 city = lookup(city_name, database())
2fe226f8 87
88 # Datetime objects for yesterday, today, tomorrow
89 today = dt.date()
90 dt = pytz.timezone(time_zone).localize(dt)
91 yesterday = today - timedelta(1)
92 tomorrow = today + timedelta(1)
93
94 # Sun objects for yesterfay, today, tomorrow
20442c9a 95 sun_yesterday = sun(city.observer, date=yesterday)
96 sun_today = sun(city.observer, date=today)
97 sun_tomorrow = sun(city.observer, date=tomorrow)
2fe226f8 98
99 # Work out sunset yesterday, sunrise/sunset today, and sunrise tomorrow
100 sunset_yesterday = sun_yesterday["sunset"]
20442c9a 101 sunrise_today = sun_today["sunrise"]
102 sunset_today = sun_today["sunset"]
2fe226f8 103 sunrise_tomorrow = sun_tomorrow["sunrise"]
104
105 # Work out lengths of day or night period and progress through period
106 if sunrise_today < dt < sunset_today:
107 day = True
108 period = sunset_today - sunrise_today
109 mid = sunrise_today + (period / 2)
110 progress = dt - sunrise_today
111
112 elif dt > sunset_today:
113 day = False
114 period = sunrise_tomorrow - sunset_today
115 mid = sunset_today + (period / 2)
116 progress = dt - sunset_today
117
118 else:
119 day = False
120 period = sunrise_today - sunset_yesterday
121 mid = sunset_yesterday + (period / 2)
122 progress = dt - sunset_yesterday
123
124 # Convert time deltas to seconds
125 progress = progress.total_seconds()
126 period = period.total_seconds()
127
128 return (progress, period, day)
129
130
131def draw_background(progress, period, day):
132 """Given an amount of progress through the day or night, draw the
133 background colour and overlay a blurred sun/moon."""
134
135 # x-coordinate for sun/moon
136 x = x_from_sun_moon_time(progress, period, WIDTH)
137
138 # If it's day, then move right to left
139 if day:
140 x = WIDTH - x
141
142 # Calculate position on sun/moon's curve
143 centre = WIDTH / 2
144 y = calculate_y_pos(x, centre)
145
146 # Background colour
147 background = map_colour(x, 80, mid_hue, day_hue, day)
148
149 # New image for background colour
150 img = Image.new('RGBA', (WIDTH, HEIGHT), color=background)
151 draw = ImageDraw.Draw(img)
152
153 # New image for sun/moon overlay
154 overlay = Image.new('RGBA', (WIDTH, HEIGHT), color=(0, 0, 0, 0))
155 overlay_draw = ImageDraw.Draw(overlay)
156
157 # Draw the sun/moon
158 circle = circle_coordinates(x, y, sun_radius)
159 overlay_draw.ellipse(circle, fill=(200, 200, 50, opacity))
160
161 # Overlay the sun/moon on the background as an alpha matte
162 composite = Image.alpha_composite(img, overlay).filter(ImageFilter.GaussianBlur(radius=blur))
163
164 return composite
165
166
167def overlay_text(img, position, text, font, align_right=False, rectangle=False):
168 draw = ImageDraw.Draw(img)
169 w, h = font.getsize(text)
170 if align_right:
171 x, y = position
172 x -= w
173 position = (x, y)
174 if rectangle:
175 x += 1
176 y += 1
177 position = (x, y)
178 border = 1
179 rect = (x - border, y, x + w, y + h + border)
180 rect_img = Image.new('RGBA', (WIDTH, HEIGHT), color=(0, 0, 0, 0))
181 rect_draw = ImageDraw.Draw(rect_img)
182 rect_draw.rectangle(rect, (255, 255, 255))
183 rect_draw.text(position, text, font=font, fill=(0, 0, 0, 0))
184 img = Image.alpha_composite(img, rect_img)
185 else:
186 draw.text(position, text, font=font, fill=(255, 255, 255))
187 return img
188
189
190def get_cpu_temperature():
191 with open("/sys/class/thermal/thermal_zone0/temp", "r") as f:
192 temp = f.read()
193 temp = int(temp) / 1000.0
194 return temp
195
196
197def correct_humidity(humidity, temperature, corr_temperature):
198 dewpoint = temperature - ((100 - humidity) / 5)
199 corr_humidity = 100 - (5 * (corr_temperature - dewpoint))
200 return min(100, corr_humidity)
201
202
203def analyse_pressure(pressure, t):
204 global time_vals, pressure_vals, trend
205 if len(pressure_vals) > num_vals:
206 pressure_vals = pressure_vals[1:] + [pressure]
207 time_vals = time_vals[1:] + [t]
208
209 # Calculate line of best fit
210 line = numpy.polyfit(time_vals, pressure_vals, 1, full=True)
211
212 # Calculate slope, variance, and confidence
213 slope = line[0][0]
214 intercept = line[0][1]
215 variance = numpy.var(pressure_vals)
216 residuals = numpy.var([(slope * x + intercept - y) for x, y in zip(time_vals, pressure_vals)])
217 r_squared = 1 - residuals / variance
218
219 # Calculate change in pressure per hour
220 change_per_hour = slope * 60 * 60
221 variance_per_hour = variance * 60 * 60
222
223 mean_pressure = numpy.mean(pressure_vals)
224
225 # Calculate trend
226 if r_squared > 0.5:
227 if change_per_hour > 0.5:
228 trend = ">"
229 elif change_per_hour < -0.5:
230 trend = "<"
231 elif -0.5 <= change_per_hour <= 0.5:
232 trend = "-"
233
234 if trend != "-":
235 if abs(change_per_hour) > 3:
236 trend *= 2
237 else:
238 pressure_vals.append(pressure)
239 time_vals.append(t)
240 mean_pressure = numpy.mean(pressure_vals)
241 change_per_hour = 0
242 trend = "-"
243
244# time.sleep(interval)
245
246 return (mean_pressure, change_per_hour, trend)
247
248def describe_pressure(pressure):
249 """Convert pressure into barometer-type description."""
250 if pressure < 970:
251 description = "storm"
252 elif 970 <= pressure < 990:
253 description = "rain"
254 elif 990 <= pressure < 1010:
255 description = "change"
256 elif 1010 <= pressure < 1030:
257 description = "fair"
258 elif pressure >= 1030:
259 description = "dry"
260 else:
261 description = ""
262 return description
263
264
265def describe_humidity(humidity):
266 """Convert relative humidity into good/bad description."""
267 if 40 < humidity < 60:
268 description = "good"
269 else:
270 description = "bad"
271 return description
272
273
274def describe_light(light):
275 """Convert light level in lux to descriptive value."""
276 if light < 50:
277 description = "dark"
278 elif 50 <= light < 100:
279 description = "dim"
280 elif 100 <= light < 500:
281 description = "light"
282 elif light >= 500:
283 description = "bright"
284 return description
285
286
287# Initialise the LCD
288disp = ST7735.ST7735(
289 port=0,
290 cs=1,
291 dc=9,
292 backlight=12,
293 rotation=270,
294 spi_speed_hz=10000000
295)
296
297disp.begin()
298
299WIDTH = disp.width
300HEIGHT = disp.height
301
302# The city and timezone that you want to display.
303city_name = "Sheffield"
304time_zone = "Europe/London"
305
306# Values that alter the look of the background
307blur = 50
308opacity = 125
309
310mid_hue = 0
311day_hue = 25
312
313sun_radius = 50
314
315# Fonts
316font_sm = ImageFont.truetype(UserFont, 12)
317font_lg = ImageFont.truetype(UserFont, 14)
318
319# Margins
320margin = 3
321
322dt = datetime.now()
323
324# Set up BME280 weather sensor
325bus = SMBus(1)
326bme280 = BME280(i2c_dev=bus)
327
20442c9a 328min_temp = None
329max_temp = None
2fe226f8 330
331factor = 2.25
332cpu_temps = [get_cpu_temperature()] * 5
333
334# Set up light sensor
335ltr559 = LTR559()
336
337# Pressure variables
338pressure_vals = []
339time_vals = []
340num_vals = 1000
341interval = 1
342trend = "-"
343
20442c9a 344# Keep track of time elapsed
345start_time = time.time()
346
2fe226f8 347while True:
20442c9a 348 path = os.path.dirname(os.path.realpath(__file__))
2fe226f8 349 dt = datetime.now()
2fe226f8 350 progress, period, day = sun_moon_time(dt, city_name, time_zone)
351 background = draw_background(progress, period, day)
352
353 # Time.
20442c9a 354 time_elapsed = time.time() - start_time
2fe226f8 355 date_string = dt.strftime("%d %b %y").lstrip('0')
356 time_string = dt.strftime("%H:%M")
357 img = overlay_text(background, (0 + margin, 0 + margin), time_string, font_lg)
358 img = overlay_text(img, (WIDTH - margin, 0 + margin), date_string, font_lg, align_right=True)
359
360 # Temperature
361 temperature = bme280.get_temperature()
362
363 # Corrected temperature
364 cpu_temp = get_cpu_temperature()
365 cpu_temps = cpu_temps[1:] + [cpu_temp]
366 avg_cpu_temp = sum(cpu_temps) / float(len(cpu_temps))
367 corr_temperature = temperature - ((avg_cpu_temp - temperature) / factor)
368
20442c9a 369 if time_elapsed > 30:
370 if min_temp is not None and max_temp is not None:
371 if corr_temperature < min_temp:
372 min_temp = corr_temperature
373 elif corr_temperature > max_temp:
374 max_temp = corr_temperature
375 else:
376 min_temp = corr_temperature
377 max_temp = corr_temperature
2fe226f8 378
379 temp_string = f"{corr_temperature:.0f}°C"
380 img = overlay_text(img, (68, 18), temp_string, font_lg, align_right=True)
381 spacing = font_lg.getsize(temp_string)[1] + 1
20442c9a 382 if min_temp is not None and max_temp is not None:
383 range_string = f"{min_temp:.0f}-{max_temp:.0f}"
384 else:
385 range_string = "------"
2fe226f8 386 img = overlay_text(img, (68, 18 + spacing), range_string, font_sm, align_right=True, rectangle=True)
20442c9a 387 temp_icon = Image.open(path + "/icons/temperature.png")
2fe226f8 388 img.paste(temp_icon, (margin, 18), mask=temp_icon)
389
390 # Humidity
391 humidity = bme280.get_humidity()
392 corr_humidity = correct_humidity(humidity, temperature, corr_temperature)
393 humidity_string = f"{corr_humidity:.0f}%"
394 img = overlay_text(img, (68, 48), humidity_string, font_lg, align_right=True)
395 spacing = font_lg.getsize(humidity_string)[1] + 1
396 humidity_desc = describe_humidity(corr_humidity).upper()
397 img = overlay_text(img, (68, 48 + spacing), humidity_desc, font_sm, align_right=True, rectangle=True)
20442c9a 398 humidity_icon = Image.open(path + "/icons/humidity-" + humidity_desc.lower() + ".png")
2fe226f8 399 img.paste(humidity_icon, (margin, 48), mask=humidity_icon)
400
401 # Light
402 light = ltr559.get_lux()
403 light_string = f"{int(light):,}"
404 img = overlay_text(img, (WIDTH - margin, 18), light_string, font_lg, align_right=True)
405 spacing = font_lg.getsize(light_string.replace(",", ""))[1] + 1
406 light_desc = describe_light(light).upper()
407 img = overlay_text(img, (WIDTH - margin - 1, 18 + spacing), light_desc, font_sm, align_right=True, rectangle=True)
20442c9a 408 light_icon = Image.open(path + "/icons/bulb-" + light_desc.lower() + ".png")
2fe226f8 409 img.paste(humidity_icon, (80, 18), mask=light_icon)
410
411 # Pressure
412 pressure = bme280.get_pressure()
413 t = time.time()
414 mean_pressure, change_per_hour, trend = analyse_pressure(pressure, t)
415 pressure_string = f"{int(mean_pressure):,} {trend}"
416 img = overlay_text(img, (WIDTH - margin, 48), pressure_string, font_lg, align_right=True)
417 pressure_desc = describe_pressure(mean_pressure).upper()
418 spacing = font_lg.getsize(pressure_string.replace(",", ""))[1] + 1
419 img = overlay_text(img, (WIDTH - margin - 1, 48 + spacing), pressure_desc, font_sm, align_right=True, rectangle=True)
20442c9a 420 pressure_icon = Image.open(path + "/icons/weather-" + pressure_desc.lower() + ".png")
2fe226f8 421 img.paste(pressure_icon, (80, 48), mask=pressure_icon)
422
423 # Display image
424 disp.display(img)