2 # -*- coding: utf-8 -*-
4 f
"Sorry! This program requires Python >= 3.6 😅"
10 from PIL
import Image
, ImageDraw
, ImageFont
, ImageFilter
11 from fonts
.ttf
import RobotoMedium
as UserFont
14 from bme280
import BME280
15 from ltr559
import LTR559
18 from pytz
import timezone
19 from astral
.geocoder
import database
, lookup
20 from astral
.sun
import sun
21 from datetime
import datetime
, timedelta
24 from smbus2
import SMBus
26 from smbus
import SMBus
29 def calculate_y_pos(x
, centre
):
30 """Calculates the y-coordinate on a parabolic curve, given x."""
32 y
= 1 / centre
* (x
- centre
) ** 2
37 def circle_coordinates(x
, y
, radius
):
38 """Calculates the bounds of a circle, given centre and radius."""
40 x1
= x
- radius
# Left
41 x2
= x
+ radius
# Right
42 y1
= y
- radius
# Bottom
45 return (x1
, y1
, x2
, y2
)
48 def map_colour(x
, centre
, start_hue
, end_hue
, day
):
49 """Given an x coordinate and a centre point, a start and end hue (in degrees),
50 and a Boolean for day or night (day is True, night False), calculate a colour
51 hue representing the 'colour' of that time of day."""
53 start_hue
= start_hue
/ 360 # Rescale to between 0 and 1
54 end_hue
= end_hue
/ 360
58 # Dim the brightness as you move from the centre to the edges
59 val
= 1 - (abs(centre
- x
) / (2 * centre
))
61 # Ramp up towards centre, then back down
66 hue
= start_hue
+ ((x
/ centre
) * (end_hue
- start_hue
))
68 # At night, move towards purple/blue hues and reverse dimming
73 r
, g
, b
= [int(c
* 255) for c
in colorsys
.hsv_to_rgb(hue
, sat
, val
)]
78 def x_from_sun_moon_time(progress
, period
, x_range
):
79 """Recalculate/rescale an amount of progress through a time period."""
81 x
= int((progress
/ period
) * x_range
)
86 def sun_moon_time(city_name
, time_zone
):
87 """Calculate the progress through the current sun/moon period (i.e day or
88 night) from the last sunrise or sunset, given a datetime object 't'."""
90 city
= lookup(city_name
, database())
92 # Datetime objects for yesterday, today, tomorrow
94 utc_dt
= datetime
.now(tz
=utc
)
95 local_dt
= utc_dt
.astimezone(pytz
.timezone(time_zone
))
96 today
= local_dt
.date()
97 yesterday
= today
- timedelta(1)
98 tomorrow
= today
+ timedelta(1)
100 # Sun objects for yesterday, today, tomorrow
101 sun_yesterday
= sun(city
.observer
, date
=yesterday
)
102 sun_today
= sun(city
.observer
, date
=today
)
103 sun_tomorrow
= sun(city
.observer
, date
=tomorrow
)
105 # Work out sunset yesterday, sunrise/sunset today, and sunrise tomorrow
106 sunset_yesterday
= sun_yesterday
["sunset"]
107 sunrise_today
= sun_today
["sunrise"]
108 sunset_today
= sun_today
["sunset"]
109 sunrise_tomorrow
= sun_tomorrow
["sunrise"]
111 # Work out lengths of day or night period and progress through period
112 if sunrise_today
< local_dt
< sunset_today
:
114 period
= sunset_today
- sunrise_today
115 # mid = sunrise_today + (period / 2)
116 progress
= local_dt
- sunrise_today
118 elif local_dt
> sunset_today
:
120 period
= sunrise_tomorrow
- sunset_today
121 # mid = sunset_today + (period / 2)
122 progress
= local_dt
- sunset_today
126 period
= sunrise_today
- sunset_yesterday
127 # mid = sunset_yesterday + (period / 2)
128 progress
= local_dt
- sunset_yesterday
130 # Convert time deltas to seconds
131 progress
= progress
.total_seconds()
132 period
= period
.total_seconds()
134 return (progress
, period
, day
, local_dt
)
137 def draw_background(progress
, period
, day
):
138 """Given an amount of progress through the day or night, draw the
139 background colour and overlay a blurred sun/moon."""
141 # x-coordinate for sun/moon
142 x
= x_from_sun_moon_time(progress
, period
, WIDTH
)
144 # If it's day, then move right to left
148 # Calculate position on sun/moon's curve
150 y
= calculate_y_pos(x
, centre
)
153 background
= map_colour(x
, 80, mid_hue
, day_hue
, day
)
155 # New image for background colour
156 img
= Image
.new('RGBA', (WIDTH
, HEIGHT
), color
=background
)
157 # draw = ImageDraw.Draw(img)
159 # New image for sun/moon overlay
160 overlay
= Image
.new('RGBA', (WIDTH
, HEIGHT
), color
=(0, 0, 0, 0))
161 overlay_draw
= ImageDraw
.Draw(overlay
)
164 circle
= circle_coordinates(x
, y
, sun_radius
)
165 overlay_draw
.ellipse(circle
, fill
=(200, 200, 50, opacity
))
167 # Overlay the sun/moon on the background as an alpha matte
168 composite
= Image
.alpha_composite(img
, overlay
).filter(ImageFilter
.GaussianBlur(radius
=blur
))
173 def overlay_text(img
, position
, text
, font
, align_right
=False, rectangle
=False):
174 draw
= ImageDraw
.Draw(img
)
175 w
, h
= font
.getsize(text
)
185 rect
= (x
- border
, y
, x
+ w
, y
+ h
+ border
)
186 rect_img
= Image
.new('RGBA', (WIDTH
, HEIGHT
), color
=(0, 0, 0, 0))
187 rect_draw
= ImageDraw
.Draw(rect_img
)
188 rect_draw
.rectangle(rect
, (255, 255, 255))
189 rect_draw
.text(position
, text
, font
=font
, fill
=(0, 0, 0, 0))
190 img
= Image
.alpha_composite(img
, rect_img
)
192 draw
.text(position
, text
, font
=font
, fill
=(255, 255, 255))
196 def get_cpu_temperature():
197 with
open("/sys/class/thermal/thermal_zone0/temp", "r") as f
:
199 temp
= int(temp
) / 1000.0
203 def correct_humidity(humidity
, temperature
, corr_temperature
):
204 dewpoint
= temperature
- ((100 - humidity
) / 5)
205 corr_humidity
= 100 - (5 * (corr_temperature
- dewpoint
))
206 return min(100, corr_humidity
)
209 def analyse_pressure(pressure
, t
):
210 global time_vals
, pressure_vals
, trend
211 if len(pressure_vals
) > num_vals
:
212 pressure_vals
= pressure_vals
[1:] + [pressure
]
213 time_vals
= time_vals
[1:] + [t
]
215 # Calculate line of best fit
216 line
= numpy
.polyfit(time_vals
, pressure_vals
, 1, full
=True)
218 # Calculate slope, variance, and confidence
220 intercept
= line
[0][1]
221 variance
= numpy
.var(pressure_vals
)
222 residuals
= numpy
.var([(slope
* x
+ intercept
- y
) for x
, y
in zip(time_vals
, pressure_vals
)])
223 r_squared
= 1 - residuals
/ variance
225 # Calculate change in pressure per hour
226 change_per_hour
= slope
* 60 * 60
227 # variance_per_hour = variance * 60 * 60
229 mean_pressure
= numpy
.mean(pressure_vals
)
233 if change_per_hour
> 0.5:
235 elif change_per_hour
< -0.5:
237 elif -0.5 <= change_per_hour
<= 0.5:
241 if abs(change_per_hour
) > 3:
244 pressure_vals
.append(pressure
)
246 mean_pressure
= numpy
.mean(pressure_vals
)
250 # time.sleep(interval)
251 return (mean_pressure
, change_per_hour
, trend
)
254 def describe_pressure(pressure
):
255 """Convert pressure into barometer-type description."""
257 description
= "storm"
258 elif 970 <= pressure
< 990:
260 elif 990 <= pressure
< 1010:
261 description
= "change"
262 elif 1010 <= pressure
< 1030:
264 elif pressure
>= 1030:
271 def describe_humidity(humidity
):
272 """Convert relative humidity into good/bad description."""
273 if 40 < humidity
< 60:
280 def describe_light(light
):
281 """Convert light level in lux to descriptive value."""
284 elif 50 <= light
< 100:
286 elif 100 <= light
< 500:
287 description
= "light"
289 description
= "bright"
294 disp
= ST7735
.ST7735(
300 spi_speed_hz
=10000000
308 # The city and timezone that you want to display.
309 city_name
= "Sheffield"
310 time_zone
= "Europe/London"
312 # Values that alter the look of the background
322 font_sm
= ImageFont
.truetype(UserFont
, 12)
323 font_lg
= ImageFont
.truetype(UserFont
, 14)
329 # Set up BME280 weather sensor
331 bme280
= BME280(i2c_dev
=bus
)
337 cpu_temps
= [get_cpu_temperature()] * 5
339 # Set up light sensor
349 # Keep track of time elapsed
350 start_time
= time
.time()
353 path
= os
.path
.dirname(os
.path
.realpath(__file__
))
354 progress
, period
, day
, local_dt
= sun_moon_time(city_name
, time_zone
)
355 background
= draw_background(progress
, period
, day
)
358 time_elapsed
= time
.time() - start_time
359 date_string
= local_dt
.strftime("%d %b %y").lstrip('0')
360 time_string
= local_dt
.strftime("%H:%M")
361 img
= overlay_text(background
, (0 + margin
, 0 + margin
), time_string
, font_lg
)
362 img
= overlay_text(img
, (WIDTH
- margin
, 0 + margin
), date_string
, font_lg
, align_right
=True)
365 temperature
= bme280
.get_temperature()
367 # Corrected temperature
368 cpu_temp
= get_cpu_temperature()
369 cpu_temps
= cpu_temps
[1:] + [cpu_temp
]
370 avg_cpu_temp
= sum(cpu_temps
) / float(len(cpu_temps
))
371 corr_temperature
= temperature
- ((avg_cpu_temp
- temperature
) / factor
)
373 if time_elapsed
> 30:
374 if min_temp
is not None and max_temp
is not None:
375 if corr_temperature
< min_temp
:
376 min_temp
= corr_temperature
377 elif corr_temperature
> max_temp
:
378 max_temp
= corr_temperature
380 min_temp
= corr_temperature
381 max_temp
= corr_temperature
383 temp_string
= f
"{corr_temperature:.0f}°C"
384 img
= overlay_text(img
, (68, 18), temp_string
, font_lg
, align_right
=True)
385 spacing
= font_lg
.getsize(temp_string
)[1] + 1
386 if min_temp
is not None and max_temp
is not None:
387 range_string
= f
"{min_temp:.0f}-{max_temp:.0f}"
389 range_string
= "------"
390 img
= overlay_text(img
, (68, 18 + spacing
), range_string
, font_sm
, align_right
=True, rectangle
=True)
391 temp_icon
= Image
.open(f
"{path}/icons/temperature.png")
392 img
.paste(temp_icon
, (margin
, 18), mask
=temp_icon
)
395 humidity
= bme280
.get_humidity()
396 corr_humidity
= correct_humidity(humidity
, temperature
, corr_temperature
)
397 humidity_string
= f
"{corr_humidity:.0f}%"
398 img
= overlay_text(img
, (68, 48), humidity_string
, font_lg
, align_right
=True)
399 spacing
= font_lg
.getsize(humidity_string
)[1] + 1
400 humidity_desc
= describe_humidity(corr_humidity
).upper()
401 img
= overlay_text(img
, (68, 48 + spacing
), humidity_desc
, font_sm
, align_right
=True, rectangle
=True)
402 humidity_icon
= Image
.open(f
"{path}/icons/humidity-{humidity_desc.lower()}.png")
403 img
.paste(humidity_icon
, (margin
, 48), mask
=humidity_icon
)
406 light
= ltr559
.get_lux()
407 light_string
= f
"{int(light):,}"
408 img
= overlay_text(img
, (WIDTH
- margin
, 18), light_string
, font_lg
, align_right
=True)
409 spacing
= font_lg
.getsize(light_string
.replace(",", ""))[1] + 1
410 light_desc
= describe_light(light
).upper()
411 img
= overlay_text(img
, (WIDTH
- margin
- 1, 18 + spacing
), light_desc
, font_sm
, align_right
=True, rectangle
=True)
412 light_icon
= Image
.open(f
"{path}/icons/bulb-{light_desc.lower()}.png")
413 img
.paste(humidity_icon
, (80, 18), mask
=light_icon
)
416 pressure
= bme280
.get_pressure()
418 mean_pressure
, change_per_hour
, trend
= analyse_pressure(pressure
, t
)
419 pressure_string
= f
"{int(mean_pressure):,} {trend}"
420 img
= overlay_text(img
, (WIDTH
- margin
, 48), pressure_string
, font_lg
, align_right
=True)
421 pressure_desc
= describe_pressure(mean_pressure
).upper()
422 spacing
= font_lg
.getsize(pressure_string
.replace(",", ""))[1] + 1
423 img
= overlay_text(img
, (WIDTH
- margin
- 1, 48 + spacing
), pressure_desc
, font_sm
, align_right
=True, rectangle
=True)
424 pressure_icon
= Image
.open(f
"{path}/icons/weather-{pressure_desc.lower()}.png")
425 img
.paste(pressure_icon
, (80, 48), mask
=pressure_icon
)