7 from PIL
import Image
, ImageDraw
, ImageFont
, ImageFilter
8 from fonts
.ttf
import RobotoMedium
as UserFont
11 from bme280
import BME280
12 from ltr559
import LTR559
15 from pytz
import timezone
16 from astral
.geocoder
import database
, lookup
17 from astral
.sun
import sun
18 from datetime
import datetime
, timedelta
21 from smbus2
import SMBus
23 from smbus
import SMBus
26 def calculate_y_pos(x
, centre
):
27 """Calculates the y-coordinate on a parabolic curve, given x."""
29 y
= 1 / centre
* (x
- centre
) ** 2
34 def circle_coordinates(x
, y
, radius
):
35 """Calculates the bounds of a circle, given centre and radius."""
37 x1
= x
- radius
# Left
38 x2
= x
+ radius
# Right
39 y1
= y
- radius
# Bottom
42 return (x1
, y1
, x2
, y2
)
45 def map_colour(x
, centre
, start_hue
, end_hue
, day
):
46 """Given an x coordinate and a centre point, a start and end hue (in degrees),
47 and a Boolean for day or night (day is True, night False), calculate a colour
48 hue representing the 'colour' of that time of day."""
50 start_hue
= start_hue
/ 360 # Rescale to between 0 and 1
51 end_hue
= end_hue
/ 360
55 # Dim the brightness as you move from the centre to the edges
56 val
= 1 - (abs(centre
- x
) / (2 * centre
))
58 # Ramp up towards centre, then back down
63 hue
= start_hue
+ ((x
/ centre
) * (end_hue
- start_hue
))
65 # At night, move towards purple/blue hues and reverse dimming
70 r
, g
, b
= [int(c
* 255) for c
in colorsys
.hsv_to_rgb(hue
, sat
, val
)]
75 def x_from_sun_moon_time(progress
, period
, x_range
):
76 """Recalculate/rescale an amount of progress through a time period."""
78 x
= int((progress
/ period
) * x_range
)
83 def sun_moon_time(city_name
, time_zone
):
84 """Calculate the progress through the current sun/moon period (i.e day or
85 night) from the last sunrise or sunset, given a datetime object 't'."""
87 city
= lookup(city_name
, database())
89 # Datetime objects for yesterday, today, tomorrow
91 utc_dt
= datetime
.now(tz
=utc
)
92 local_dt
= utc_dt
.astimezone(pytz
.timezone(time_zone
))
93 today
= local_dt
.date()
94 yesterday
= today
- timedelta(1)
95 tomorrow
= today
+ timedelta(1)
97 # Sun objects for yesterday, today, tomorrow
98 sun_yesterday
= sun(city
.observer
, date
=yesterday
)
99 sun_today
= sun(city
.observer
, date
=today
)
100 sun_tomorrow
= sun(city
.observer
, date
=tomorrow
)
102 # Work out sunset yesterday, sunrise/sunset today, and sunrise tomorrow
103 sunset_yesterday
= sun_yesterday
["sunset"]
104 sunrise_today
= sun_today
["sunrise"]
105 sunset_today
= sun_today
["sunset"]
106 sunrise_tomorrow
= sun_tomorrow
["sunrise"]
108 # Work out lengths of day or night period and progress through period
109 if sunrise_today
< local_dt
< sunset_today
:
111 period
= sunset_today
- sunrise_today
112 # mid = sunrise_today + (period / 2)
113 progress
= local_dt
- sunrise_today
115 elif local_dt
> sunset_today
:
117 period
= sunrise_tomorrow
- sunset_today
118 # mid = sunset_today + (period / 2)
119 progress
= local_dt
- sunset_today
123 period
= sunrise_today
- sunset_yesterday
124 # mid = sunset_yesterday + (period / 2)
125 progress
= local_dt
- sunset_yesterday
127 # Convert time deltas to seconds
128 progress
= progress
.total_seconds()
129 period
= period
.total_seconds()
131 return (progress
, period
, day
, local_dt
)
134 def draw_background(progress
, period
, day
):
135 """Given an amount of progress through the day or night, draw the
136 background colour and overlay a blurred sun/moon."""
138 # x-coordinate for sun/moon
139 x
= x_from_sun_moon_time(progress
, period
, WIDTH
)
141 # If it's day, then move right to left
145 # Calculate position on sun/moon's curve
147 y
= calculate_y_pos(x
, centre
)
150 background
= map_colour(x
, 80, mid_hue
, day_hue
, day
)
152 # New image for background colour
153 img
= Image
.new('RGBA', (WIDTH
, HEIGHT
), color
=background
)
154 # draw = ImageDraw.Draw(img)
156 # New image for sun/moon overlay
157 overlay
= Image
.new('RGBA', (WIDTH
, HEIGHT
), color
=(0, 0, 0, 0))
158 overlay_draw
= ImageDraw
.Draw(overlay
)
161 circle
= circle_coordinates(x
, y
, sun_radius
)
162 overlay_draw
.ellipse(circle
, fill
=(200, 200, 50, opacity
))
164 # Overlay the sun/moon on the background as an alpha matte
165 composite
= Image
.alpha_composite(img
, overlay
).filter(ImageFilter
.GaussianBlur(radius
=blur
))
170 def overlay_text(img
, position
, text
, font
, align_right
=False, rectangle
=False):
171 draw
= ImageDraw
.Draw(img
)
172 w
, h
= font
.getsize(text
)
182 rect
= (x
- border
, y
, x
+ w
, y
+ h
+ border
)
183 rect_img
= Image
.new('RGBA', (WIDTH
, HEIGHT
), color
=(0, 0, 0, 0))
184 rect_draw
= ImageDraw
.Draw(rect_img
)
185 rect_draw
.rectangle(rect
, (255, 255, 255))
186 rect_draw
.text(position
, text
, font
=font
, fill
=(0, 0, 0, 0))
187 img
= Image
.alpha_composite(img
, rect_img
)
189 draw
.text(position
, text
, font
=font
, fill
=(255, 255, 255))
193 def get_cpu_temperature():
194 with
open("/sys/class/thermal/thermal_zone0/temp", "r") as f
:
196 temp
= int(temp
) / 1000.0
200 def correct_humidity(humidity
, temperature
, corr_temperature
):
201 dewpoint
= temperature
- ((100 - humidity
) / 5)
202 corr_humidity
= 100 - (5 * (corr_temperature
- dewpoint
))
203 return min(100, corr_humidity
)
206 def analyse_pressure(pressure
, t
):
207 global time_vals
, pressure_vals
, trend
208 if len(pressure_vals
) > num_vals
:
209 pressure_vals
= pressure_vals
[1:] + [pressure
]
210 time_vals
= time_vals
[1:] + [t
]
212 # Calculate line of best fit
213 line
= numpy
.polyfit(time_vals
, pressure_vals
, 1, full
=True)
215 # Calculate slope, variance, and confidence
217 intercept
= line
[0][1]
218 variance
= numpy
.var(pressure_vals
)
219 residuals
= numpy
.var([(slope
* x
+ intercept
- y
) for x
, y
in zip(time_vals
, pressure_vals
)])
220 r_squared
= 1 - residuals
/ variance
222 # Calculate change in pressure per hour
223 change_per_hour
= slope
* 60 * 60
224 # variance_per_hour = variance * 60 * 60
226 mean_pressure
= numpy
.mean(pressure_vals
)
230 if change_per_hour
> 0.5:
232 elif change_per_hour
< -0.5:
234 elif -0.5 <= change_per_hour
<= 0.5:
238 if abs(change_per_hour
) > 3:
241 pressure_vals
.append(pressure
)
243 mean_pressure
= numpy
.mean(pressure_vals
)
247 # time.sleep(interval)
248 return (mean_pressure
, change_per_hour
, trend
)
251 def describe_pressure(pressure
):
252 """Convert pressure into barometer-type description."""
254 description
= "storm"
255 elif 970 <= pressure
< 990:
257 elif 990 <= pressure
< 1010:
258 description
= "change"
259 elif 1010 <= pressure
< 1030:
261 elif pressure
>= 1030:
268 def describe_humidity(humidity
):
269 """Convert relative humidity into good/bad description."""
270 if 40 < humidity
< 60:
277 def describe_light(light
):
278 """Convert light level in lux to descriptive value."""
281 elif 50 <= light
< 100:
283 elif 100 <= light
< 500:
284 description
= "light"
286 description
= "bright"
291 disp
= ST7735
.ST7735(
297 spi_speed_hz
=10000000
305 # The city and timezone that you want to display.
306 city_name
= "Sheffield"
307 time_zone
= "Europe/London"
309 # Values that alter the look of the background
319 font_sm
= ImageFont
.truetype(UserFont
, 12)
320 font_lg
= ImageFont
.truetype(UserFont
, 14)
326 # Set up BME280 weather sensor
328 bme280
= BME280(i2c_dev
=bus
)
334 cpu_temps
= [get_cpu_temperature()] * 5
336 # Set up light sensor
346 # Keep track of time elapsed
347 start_time
= time
.time()
350 path
= os
.path
.dirname(os
.path
.realpath(__file__
))
351 progress
, period
, day
, local_dt
= sun_moon_time(city_name
, time_zone
)
352 background
= draw_background(progress
, period
, day
)
355 time_elapsed
= time
.time() - start_time
356 date_string
= local_dt
.strftime("%d %b %y").lstrip('0')
357 time_string
= local_dt
.strftime("%H:%M")
358 img
= overlay_text(background
, (0 + margin
, 0 + margin
), time_string
, font_lg
)
359 img
= overlay_text(img
, (WIDTH
- margin
, 0 + margin
), date_string
, font_lg
, align_right
=True)
362 temperature
= bme280
.get_temperature()
364 # Corrected temperature
365 cpu_temp
= get_cpu_temperature()
366 cpu_temps
= cpu_temps
[1:] + [cpu_temp
]
367 avg_cpu_temp
= sum(cpu_temps
) / float(len(cpu_temps
))
368 corr_temperature
= temperature
- ((avg_cpu_temp
- temperature
) / factor
)
370 if time_elapsed
> 30:
371 if min_temp
is not None and max_temp
is not None:
372 if corr_temperature
< min_temp
:
373 min_temp
= corr_temperature
374 elif corr_temperature
> max_temp
:
375 max_temp
= corr_temperature
377 min_temp
= corr_temperature
378 max_temp
= corr_temperature
380 temp_string
= f
"{corr_temperature:.0f}°C"
381 img
= overlay_text(img
, (68, 18), temp_string
, font_lg
, align_right
=True)
382 spacing
= font_lg
.getsize(temp_string
)[1] + 1
383 if min_temp
is not None and max_temp
is not None:
384 range_string
= f
"{min_temp:.0f}-{max_temp:.0f}"
386 range_string
= "------"
387 img
= overlay_text(img
, (68, 18 + spacing
), range_string
, font_sm
, align_right
=True, rectangle
=True)
388 temp_icon
= Image
.open(f
"{path}/icons/temperature.png")
389 img
.paste(temp_icon
, (margin
, 18), mask
=temp_icon
)
392 humidity
= bme280
.get_humidity()
393 corr_humidity
= correct_humidity(humidity
, temperature
, corr_temperature
)
394 humidity_string
= f
"{corr_humidity:.0f}%"
395 img
= overlay_text(img
, (68, 48), humidity_string
, font_lg
, align_right
=True)
396 spacing
= font_lg
.getsize(humidity_string
)[1] + 1
397 humidity_desc
= describe_humidity(corr_humidity
).upper()
398 img
= overlay_text(img
, (68, 48 + spacing
), humidity_desc
, font_sm
, align_right
=True, rectangle
=True)
399 humidity_icon
= Image
.open(f
"{path}/icons/humidity-{humidity_desc.lower()}.png")
400 img
.paste(humidity_icon
, (margin
, 48), mask
=humidity_icon
)
403 light
= ltr559
.get_lux()
404 light_string
= f
"{int(light):,}"
405 img
= overlay_text(img
, (WIDTH
- margin
, 18), light_string
, font_lg
, align_right
=True)
406 spacing
= font_lg
.getsize(light_string
.replace(",", ""))[1] + 1
407 light_desc
= describe_light(light
).upper()
408 img
= overlay_text(img
, (WIDTH
- margin
- 1, 18 + spacing
), light_desc
, font_sm
, align_right
=True, rectangle
=True)
409 light_icon
= Image
.open(f
"{path}/icons/bulb-{light_desc.lower()}.png")
410 img
.paste(humidity_icon
, (80, 18), mask
=light_icon
)
413 pressure
= bme280
.get_pressure()
415 mean_pressure
, change_per_hour
, trend
= analyse_pressure(pressure
, t
)
416 pressure_string
= f
"{int(mean_pressure):,} {trend}"
417 img
= overlay_text(img
, (WIDTH
- margin
, 48), pressure_string
, font_lg
, align_right
=True)
418 pressure_desc
= describe_pressure(mean_pressure
).upper()
419 spacing
= font_lg
.getsize(pressure_string
.replace(",", ""))[1] + 1
420 img
= overlay_text(img
, (WIDTH
- margin
- 1, 48 + spacing
), pressure_desc
, font_sm
, align_right
=True, rectangle
=True)
421 pressure_icon
= Image
.open(f
"{path}/icons/weather-{pressure_desc.lower()}.png")
422 img
.paste(pressure_icon
, (80, 48), mask
=pressure_icon
)