4 from PIL
import Image
, ImageDraw
, ImageFont
, ImageFilter
5 from fonts
.ttf
import RobotoMedium
as UserFont
8 from bme280
import BME280
9 from ltr559
import LTR559
12 from astral
import Astral
13 from datetime
import datetime
, timedelta
16 from smbus2
import SMBus
18 from smbus
import SMBus
21 def calculate_y_pos(x
, centre
):
22 """Calculates the y-coordinate on a parabolic curve, given x."""
24 y
= 1 / centre
* (x
- centre
) ** 2
29 def circle_coordinates(x
, y
, radius
):
30 """Calculates the bounds of a circle, given centre and radius."""
32 x1
= x
- radius
# Left
33 x2
= x
+ radius
# Right
34 y1
= y
- radius
# Bottom
37 return (x1
, y1
, x2
, y2
)
40 def map_colour(x
, centre
, start_hue
, end_hue
, day
):
41 """Given an x coordinate and a centre point, a start and end hue (in degrees),
42 and a Boolean for day or night (day is True, night False), calculate a colour
43 hue representing the 'colour' of that time of day."""
45 start_hue
= start_hue
/ 360 # Rescale to between 0 and 1
46 end_hue
= end_hue
/ 360
50 # Dim the brightness as you move from the centre to the edges
51 val
= 1 - (abs(centre
- x
) / (2 * centre
))
53 # Ramp up towards centre, then back down
58 hue
= start_hue
+ ((x
/ centre
) * (end_hue
- start_hue
))
60 # At night, move towards purple/blue hues and reverse dimming
65 r
, g
, b
= [int(c
* 255) for c
in colorsys
.hsv_to_rgb(hue
, sat
, val
)]
70 def x_from_sun_moon_time(progress
, period
, x_range
):
71 """Recalculate/rescale an amount of progress through a time period."""
73 x
= int((progress
/ period
) * x_range
)
78 def sun_moon_time(dt
, city_name
, time_zone
):
79 """Calculate the progress through the current sun/moon period (i.e day or
80 night) from the last sunrise or sunset, given a datetime object 't'."""
85 # Datetime objects for yesterday, today, tomorrow
87 dt
= pytz
.timezone(time_zone
).localize(dt
)
88 yesterday
= today
- timedelta(1)
89 tomorrow
= today
+ timedelta(1)
91 # Sun objects for yesterfay, today, tomorrow
92 sun_yesterday
= city
.sun(date
=yesterday
, local
=True)
93 sun
= city
.sun(date
=today
, local
=True)
94 sun_tomorrow
= city
.sun(date
=tomorrow
, local
=True)
96 # Work out sunset yesterday, sunrise/sunset today, and sunrise tomorrow
97 sunset_yesterday
= sun_yesterday
["sunset"]
98 sunrise_today
= sun
["sunrise"]
99 sunset_today
= sun
["sunset"]
100 sunrise_tomorrow
= sun_tomorrow
["sunrise"]
102 # Work out lengths of day or night period and progress through period
103 if sunrise_today
< dt
< sunset_today
:
105 period
= sunset_today
- sunrise_today
106 mid
= sunrise_today
+ (period
/ 2)
107 progress
= dt
- sunrise_today
109 elif dt
> sunset_today
:
111 period
= sunrise_tomorrow
- sunset_today
112 mid
= sunset_today
+ (period
/ 2)
113 progress
= dt
- sunset_today
117 period
= sunrise_today
- sunset_yesterday
118 mid
= sunset_yesterday
+ (period
/ 2)
119 progress
= dt
- sunset_yesterday
121 # Convert time deltas to seconds
122 progress
= progress
.total_seconds()
123 period
= period
.total_seconds()
125 return (progress
, period
, day
)
128 def draw_background(progress
, period
, day
):
129 """Given an amount of progress through the day or night, draw the
130 background colour and overlay a blurred sun/moon."""
132 # x-coordinate for sun/moon
133 x
= x_from_sun_moon_time(progress
, period
, WIDTH
)
135 # If it's day, then move right to left
139 # Calculate position on sun/moon's curve
141 y
= calculate_y_pos(x
, centre
)
144 background
= map_colour(x
, 80, mid_hue
, day_hue
, day
)
146 # New image for background colour
147 img
= Image
.new('RGBA', (WIDTH
, HEIGHT
), color
=background
)
148 draw
= ImageDraw
.Draw(img
)
150 # New image for sun/moon overlay
151 overlay
= Image
.new('RGBA', (WIDTH
, HEIGHT
), color
=(0, 0, 0, 0))
152 overlay_draw
= ImageDraw
.Draw(overlay
)
155 circle
= circle_coordinates(x
, y
, sun_radius
)
156 overlay_draw
.ellipse(circle
, fill
=(200, 200, 50, opacity
))
158 # Overlay the sun/moon on the background as an alpha matte
159 composite
= Image
.alpha_composite(img
, overlay
).filter(ImageFilter
.GaussianBlur(radius
=blur
))
164 def overlay_text(img
, position
, text
, font
, align_right
=False, rectangle
=False):
165 draw
= ImageDraw
.Draw(img
)
166 w
, h
= font
.getsize(text
)
176 rect
= (x
- border
, y
, x
+ w
, y
+ h
+ border
)
177 rect_img
= Image
.new('RGBA', (WIDTH
, HEIGHT
), color
=(0, 0, 0, 0))
178 rect_draw
= ImageDraw
.Draw(rect_img
)
179 rect_draw
.rectangle(rect
, (255, 255, 255))
180 rect_draw
.text(position
, text
, font
=font
, fill
=(0, 0, 0, 0))
181 img
= Image
.alpha_composite(img
, rect_img
)
183 draw
.text(position
, text
, font
=font
, fill
=(255, 255, 255))
187 def get_cpu_temperature():
188 with
open("/sys/class/thermal/thermal_zone0/temp", "r") as f
:
190 temp
= int(temp
) / 1000.0
194 def correct_humidity(humidity
, temperature
, corr_temperature
):
195 dewpoint
= temperature
- ((100 - humidity
) / 5)
196 corr_humidity
= 100 - (5 * (corr_temperature
- dewpoint
))
197 return min(100, corr_humidity
)
200 def analyse_pressure(pressure
, t
):
201 global time_vals
, pressure_vals
, trend
202 if len(pressure_vals
) > num_vals
:
203 pressure_vals
= pressure_vals
[1:] + [pressure
]
204 time_vals
= time_vals
[1:] + [t
]
206 # Calculate line of best fit
207 line
= numpy
.polyfit(time_vals
, pressure_vals
, 1, full
=True)
209 # Calculate slope, variance, and confidence
211 intercept
= line
[0][1]
212 variance
= numpy
.var(pressure_vals
)
213 residuals
= numpy
.var([(slope
* x
+ intercept
- y
) for x
, y
in zip(time_vals
, pressure_vals
)])
214 r_squared
= 1 - residuals
/ variance
216 # Calculate change in pressure per hour
217 change_per_hour
= slope
* 60 * 60
218 variance_per_hour
= variance
* 60 * 60
220 mean_pressure
= numpy
.mean(pressure_vals
)
224 if change_per_hour
> 0.5:
226 elif change_per_hour
< -0.5:
228 elif -0.5 <= change_per_hour
<= 0.5:
232 if abs(change_per_hour
) > 3:
235 pressure_vals
.append(pressure
)
237 mean_pressure
= numpy
.mean(pressure_vals
)
241 # time.sleep(interval)
243 return (mean_pressure
, change_per_hour
, trend
)
245 def describe_pressure(pressure
):
246 """Convert pressure into barometer-type description."""
248 description
= "storm"
249 elif 970 <= pressure
< 990:
251 elif 990 <= pressure
< 1010:
252 description
= "change"
253 elif 1010 <= pressure
< 1030:
255 elif pressure
>= 1030:
262 def describe_humidity(humidity
):
263 """Convert relative humidity into good/bad description."""
264 if 40 < humidity
< 60:
271 def describe_light(light
):
272 """Convert light level in lux to descriptive value."""
275 elif 50 <= light
< 100:
277 elif 100 <= light
< 500:
278 description
= "light"
280 description
= "bright"
285 disp
= ST7735
.ST7735(
291 spi_speed_hz
=10000000
299 # The city and timezone that you want to display.
300 city_name
= "Sheffield"
301 time_zone
= "Europe/London"
303 # Values that alter the look of the background
313 font_sm
= ImageFont
.truetype(UserFont
, 12)
314 font_lg
= ImageFont
.truetype(UserFont
, 14)
321 # Set up BME280 weather sensor
323 bme280
= BME280(i2c_dev
=bus
)
325 min_temp
= bme280
.get_temperature()
326 max_temp
= bme280
.get_temperature()
329 cpu_temps
= [get_cpu_temperature()] * 5
331 # Set up light sensor
343 # dt += timedelta(minutes=5)
344 progress
, period
, day
= sun_moon_time(dt
, city_name
, time_zone
)
345 background
= draw_background(progress
, period
, day
)
348 date_string
= dt
.strftime("%d %b %y").lstrip('0')
349 time_string
= dt
.strftime("%H:%M")
350 img
= overlay_text(background
, (0 + margin
, 0 + margin
), time_string
, font_lg
)
351 img
= overlay_text(img
, (WIDTH
- margin
, 0 + margin
), date_string
, font_lg
, align_right
=True)
354 temperature
= bme280
.get_temperature()
356 # Corrected temperature
357 cpu_temp
= get_cpu_temperature()
358 cpu_temps
= cpu_temps
[1:] + [cpu_temp
]
359 avg_cpu_temp
= sum(cpu_temps
) / float(len(cpu_temps
))
360 corr_temperature
= temperature
- ((avg_cpu_temp
- temperature
) / factor
)
362 if corr_temperature
< min_temp
:
363 min_temp
= corr_temperature
364 elif corr_temperature
> max_temp
:
365 max_temp
= corr_temperature
367 temp_string
= f
"{corr_temperature:.0f}°C"
368 img
= overlay_text(img
, (68, 18), temp_string
, font_lg
, align_right
=True)
369 spacing
= font_lg
.getsize(temp_string
)[1] + 1
370 range_string
= f
"{min_temp:.0f}-{max_temp:.0f}"
371 img
= overlay_text(img
, (68, 18 + spacing
), range_string
, font_sm
, align_right
=True, rectangle
=True)
372 temp_icon
= Image
.open("icons/temperature.png")
373 img
.paste(temp_icon
, (margin
, 18), mask
=temp_icon
)
376 humidity
= bme280
.get_humidity()
377 corr_humidity
= correct_humidity(humidity
, temperature
, corr_temperature
)
378 humidity_string
= f
"{corr_humidity:.0f}%"
379 img
= overlay_text(img
, (68, 48), humidity_string
, font_lg
, align_right
=True)
380 spacing
= font_lg
.getsize(humidity_string
)[1] + 1
381 humidity_desc
= describe_humidity(corr_humidity
).upper()
382 img
= overlay_text(img
, (68, 48 + spacing
), humidity_desc
, font_sm
, align_right
=True, rectangle
=True)
383 humidity_icon
= Image
.open("icons/humidity-" + humidity_desc
.lower() + ".png")
384 img
.paste(humidity_icon
, (margin
, 48), mask
=humidity_icon
)
387 light
= ltr559
.get_lux()
388 light_string
= f
"{int(light):,}"
389 img
= overlay_text(img
, (WIDTH
- margin
, 18), light_string
, font_lg
, align_right
=True)
390 spacing
= font_lg
.getsize(light_string
.replace(",", ""))[1] + 1
391 light_desc
= describe_light(light
).upper()
392 img
= overlay_text(img
, (WIDTH
- margin
- 1, 18 + spacing
), light_desc
, font_sm
, align_right
=True, rectangle
=True)
393 light_icon
= Image
.open("icons/bulb-" + light_desc
.lower() + ".png")
394 img
.paste(humidity_icon
, (80, 18), mask
=light_icon
)
397 pressure
= bme280
.get_pressure()
399 mean_pressure
, change_per_hour
, trend
= analyse_pressure(pressure
, t
)
400 pressure_string
= f
"{int(mean_pressure):,} {trend}"
401 img
= overlay_text(img
, (WIDTH
- margin
, 48), pressure_string
, font_lg
, align_right
=True)
402 pressure_desc
= describe_pressure(mean_pressure
).upper()
403 spacing
= font_lg
.getsize(pressure_string
.replace(",", ""))[1] + 1
404 img
= overlay_text(img
, (WIDTH
- margin
- 1, 48 + spacing
), pressure_desc
, font_sm
, align_right
=True, rectangle
=True)
405 pressure_icon
= Image
.open("icons/weather-" + pressure_desc
.lower() + ".png")
406 img
.paste(pressure_icon
, (80, 48), mask
=pressure_icon
)