6 from PIL
import Image
, ImageDraw
, ImageFont
, ImageFilter
7 from fonts
.ttf
import RobotoMedium
as UserFont
10 from bme280
import BME280
11 from ltr559
import LTR559
14 from astral
import Astral
15 from datetime
import datetime
, timedelta
18 from smbus2
import SMBus
20 from smbus
import SMBus
23 def calculate_y_pos(x
, centre
):
24 """Calculates the y-coordinate on a parabolic curve, given x."""
26 y
= 1 / centre
* (x
- centre
) ** 2
31 def circle_coordinates(x
, y
, radius
):
32 """Calculates the bounds of a circle, given centre and radius."""
34 x1
= x
- radius
# Left
35 x2
= x
+ radius
# Right
36 y1
= y
- radius
# Bottom
39 return (x1
, y1
, x2
, y2
)
42 def map_colour(x
, centre
, start_hue
, end_hue
, day
):
43 """Given an x coordinate and a centre point, a start and end hue (in degrees),
44 and a Boolean for day or night (day is True, night False), calculate a colour
45 hue representing the 'colour' of that time of day."""
47 start_hue
= start_hue
/ 360 # Rescale to between 0 and 1
48 end_hue
= end_hue
/ 360
52 # Dim the brightness as you move from the centre to the edges
53 val
= 1 - (abs(centre
- x
) / (2 * centre
))
55 # Ramp up towards centre, then back down
60 hue
= start_hue
+ ((x
/ centre
) * (end_hue
- start_hue
))
62 # At night, move towards purple/blue hues and reverse dimming
67 r
, g
, b
= [int(c
* 255) for c
in colorsys
.hsv_to_rgb(hue
, sat
, val
)]
72 def x_from_sun_moon_time(progress
, period
, x_range
):
73 """Recalculate/rescale an amount of progress through a time period."""
75 x
= int((progress
/ period
) * x_range
)
80 def sun_moon_time(dt
, city_name
, time_zone
):
81 """Calculate the progress through the current sun/moon period (i.e day or
82 night) from the last sunrise or sunset, given a datetime object 't'."""
87 # Datetime objects for yesterday, today, tomorrow
89 dt
= pytz
.timezone(time_zone
).localize(dt
)
90 yesterday
= today
- timedelta(1)
91 tomorrow
= today
+ timedelta(1)
93 # Sun objects for yesterfay, today, tomorrow
94 sun_yesterday
= city
.sun(date
=yesterday
, local
=True)
95 sun
= city
.sun(date
=today
, local
=True)
96 sun_tomorrow
= city
.sun(date
=tomorrow
, local
=True)
98 # Work out sunset yesterday, sunrise/sunset today, and sunrise tomorrow
99 sunset_yesterday
= sun_yesterday
["sunset"]
100 sunrise_today
= sun
["sunrise"]
101 sunset_today
= sun
["sunset"]
102 sunrise_tomorrow
= sun_tomorrow
["sunrise"]
104 # Work out lengths of day or night period and progress through period
105 if sunrise_today
< dt
< sunset_today
:
107 period
= sunset_today
- sunrise_today
108 mid
= sunrise_today
+ (period
/ 2)
109 progress
= dt
- sunrise_today
111 elif dt
> sunset_today
:
113 period
= sunrise_tomorrow
- sunset_today
114 mid
= sunset_today
+ (period
/ 2)
115 progress
= dt
- sunset_today
119 period
= sunrise_today
- sunset_yesterday
120 mid
= sunset_yesterday
+ (period
/ 2)
121 progress
= dt
- sunset_yesterday
123 # Convert time deltas to seconds
124 progress
= progress
.total_seconds()
125 period
= period
.total_seconds()
127 return (progress
, period
, day
)
130 def draw_background(progress
, period
, day
):
131 """Given an amount of progress through the day or night, draw the
132 background colour and overlay a blurred sun/moon."""
134 # x-coordinate for sun/moon
135 x
= x_from_sun_moon_time(progress
, period
, WIDTH
)
137 # If it's day, then move right to left
141 # Calculate position on sun/moon's curve
143 y
= calculate_y_pos(x
, centre
)
146 background
= map_colour(x
, 80, mid_hue
, day_hue
, day
)
148 # New image for background colour
149 img
= Image
.new('RGBA', (WIDTH
, HEIGHT
), color
=background
)
150 draw
= ImageDraw
.Draw(img
)
152 # New image for sun/moon overlay
153 overlay
= Image
.new('RGBA', (WIDTH
, HEIGHT
), color
=(0, 0, 0, 0))
154 overlay_draw
= ImageDraw
.Draw(overlay
)
157 circle
= circle_coordinates(x
, y
, sun_radius
)
158 overlay_draw
.ellipse(circle
, fill
=(200, 200, 50, opacity
))
160 # Overlay the sun/moon on the background as an alpha matte
161 composite
= Image
.alpha_composite(img
, overlay
).filter(ImageFilter
.GaussianBlur(radius
=blur
))
166 def overlay_text(img
, position
, text
, font
, align_right
=False, rectangle
=False):
167 draw
= ImageDraw
.Draw(img
)
168 w
, h
= font
.getsize(text
)
178 rect
= (x
- border
, y
, x
+ w
, y
+ h
+ border
)
179 rect_img
= Image
.new('RGBA', (WIDTH
, HEIGHT
), color
=(0, 0, 0, 0))
180 rect_draw
= ImageDraw
.Draw(rect_img
)
181 rect_draw
.rectangle(rect
, (255, 255, 255))
182 rect_draw
.text(position
, text
, font
=font
, fill
=(0, 0, 0, 0))
183 img
= Image
.alpha_composite(img
, rect_img
)
185 draw
.text(position
, text
, font
=font
, fill
=(255, 255, 255))
189 def get_cpu_temperature():
190 with
open("/sys/class/thermal/thermal_zone0/temp", "r") as f
:
192 temp
= int(temp
) / 1000.0
196 def correct_humidity(humidity
, temperature
, corr_temperature
):
197 dewpoint
= temperature
- ((100 - humidity
) / 5)
198 corr_humidity
= 100 - (5 * (corr_temperature
- dewpoint
))
199 return min(100, corr_humidity
)
202 def analyse_pressure(pressure
, t
):
203 global time_vals
, pressure_vals
, trend
204 if len(pressure_vals
) > num_vals
:
205 pressure_vals
= pressure_vals
[1:] + [pressure
]
206 time_vals
= time_vals
[1:] + [t
]
208 # Calculate line of best fit
209 line
= numpy
.polyfit(time_vals
, pressure_vals
, 1, full
=True)
211 # Calculate slope, variance, and confidence
213 intercept
= line
[0][1]
214 variance
= numpy
.var(pressure_vals
)
215 residuals
= numpy
.var([(slope
* x
+ intercept
- y
) for x
, y
in zip(time_vals
, pressure_vals
)])
216 r_squared
= 1 - residuals
/ variance
218 # Calculate change in pressure per hour
219 change_per_hour
= slope
* 60 * 60
220 variance_per_hour
= variance
* 60 * 60
222 mean_pressure
= numpy
.mean(pressure_vals
)
226 if change_per_hour
> 0.5:
228 elif change_per_hour
< -0.5:
230 elif -0.5 <= change_per_hour
<= 0.5:
234 if abs(change_per_hour
) > 3:
237 pressure_vals
.append(pressure
)
239 mean_pressure
= numpy
.mean(pressure_vals
)
243 # time.sleep(interval)
245 return (mean_pressure
, change_per_hour
, trend
)
247 def describe_pressure(pressure
):
248 """Convert pressure into barometer-type description."""
250 description
= "storm"
251 elif 970 <= pressure
< 990:
253 elif 990 <= pressure
< 1010:
254 description
= "change"
255 elif 1010 <= pressure
< 1030:
257 elif pressure
>= 1030:
264 def describe_humidity(humidity
):
265 """Convert relative humidity into good/bad description."""
266 if 40 < humidity
< 60:
273 def describe_light(light
):
274 """Convert light level in lux to descriptive value."""
277 elif 50 <= light
< 100:
279 elif 100 <= light
< 500:
280 description
= "light"
282 description
= "bright"
287 disp
= ST7735
.ST7735(
293 spi_speed_hz
=10000000
301 # The city and timezone that you want to display.
302 city_name
= "Sheffield"
303 time_zone
= "Europe/London"
305 # Values that alter the look of the background
315 font_sm
= ImageFont
.truetype(UserFont
, 12)
316 font_lg
= ImageFont
.truetype(UserFont
, 14)
323 # Set up BME280 weather sensor
325 bme280
= BME280(i2c_dev
=bus
)
327 min_temp
= bme280
.get_temperature()
328 max_temp
= bme280
.get_temperature()
331 cpu_temps
= [get_cpu_temperature()] * 5
333 # Set up light sensor
345 # dt += timedelta(minutes=5)
346 progress
, period
, day
= sun_moon_time(dt
, city_name
, time_zone
)
347 background
= draw_background(progress
, period
, day
)
350 date_string
= dt
.strftime("%d %b %y").lstrip('0')
351 time_string
= dt
.strftime("%H:%M")
352 img
= overlay_text(background
, (0 + margin
, 0 + margin
), time_string
, font_lg
)
353 img
= overlay_text(img
, (WIDTH
- margin
, 0 + margin
), date_string
, font_lg
, align_right
=True)
356 temperature
= bme280
.get_temperature()
358 # Corrected temperature
359 cpu_temp
= get_cpu_temperature()
360 cpu_temps
= cpu_temps
[1:] + [cpu_temp
]
361 avg_cpu_temp
= sum(cpu_temps
) / float(len(cpu_temps
))
362 corr_temperature
= temperature
- ((avg_cpu_temp
- temperature
) / factor
)
364 if corr_temperature
< min_temp
:
365 min_temp
= corr_temperature
366 elif corr_temperature
> max_temp
:
367 max_temp
= corr_temperature
369 temp_string
= f
"{corr_temperature:.0f}°C"
370 img
= overlay_text(img
, (68, 18), temp_string
, font_lg
, align_right
=True)
371 spacing
= font_lg
.getsize(temp_string
)[1] + 1
372 range_string
= f
"{min_temp:.0f}-{max_temp:.0f}"
373 img
= overlay_text(img
, (68, 18 + spacing
), range_string
, font_sm
, align_right
=True, rectangle
=True)
374 temp_icon
= Image
.open("icons/temperature.png")
375 img
.paste(temp_icon
, (margin
, 18), mask
=temp_icon
)
378 humidity
= bme280
.get_humidity()
379 corr_humidity
= correct_humidity(humidity
, temperature
, corr_temperature
)
380 humidity_string
= f
"{corr_humidity:.0f}%"
381 img
= overlay_text(img
, (68, 48), humidity_string
, font_lg
, align_right
=True)
382 spacing
= font_lg
.getsize(humidity_string
)[1] + 1
383 humidity_desc
= describe_humidity(corr_humidity
).upper()
384 img
= overlay_text(img
, (68, 48 + spacing
), humidity_desc
, font_sm
, align_right
=True, rectangle
=True)
385 humidity_icon
= Image
.open("icons/humidity-" + humidity_desc
.lower() + ".png")
386 img
.paste(humidity_icon
, (margin
, 48), mask
=humidity_icon
)
389 light
= ltr559
.get_lux()
390 light_string
= f
"{int(light):,}"
391 img
= overlay_text(img
, (WIDTH
- margin
, 18), light_string
, font_lg
, align_right
=True)
392 spacing
= font_lg
.getsize(light_string
.replace(",", ""))[1] + 1
393 light_desc
= describe_light(light
).upper()
394 img
= overlay_text(img
, (WIDTH
- margin
- 1, 18 + spacing
), light_desc
, font_sm
, align_right
=True, rectangle
=True)
395 light_icon
= Image
.open("icons/bulb-" + light_desc
.lower() + ".png")
396 img
.paste(humidity_icon
, (80, 18), mask
=light_icon
)
399 pressure
= bme280
.get_pressure()
401 mean_pressure
, change_per_hour
, trend
= analyse_pressure(pressure
, t
)
402 pressure_string
= f
"{int(mean_pressure):,} {trend}"
403 img
= overlay_text(img
, (WIDTH
- margin
, 48), pressure_string
, font_lg
, align_right
=True)
404 pressure_desc
= describe_pressure(mean_pressure
).upper()
405 spacing
= font_lg
.getsize(pressure_string
.replace(",", ""))[1] + 1
406 img
= overlay_text(img
, (WIDTH
- margin
- 1, 48 + spacing
), pressure_desc
, font_sm
, align_right
=True, rectangle
=True)
407 pressure_icon
= Image
.open("icons/weather-" + pressure_desc
.lower() + ".png")
408 img
.paste(pressure_icon
, (80, 48), mask
=pressure_icon
)