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 astral
.geocoder
import database
, lookup
16 from astral
.sun
import sun
17 from datetime
import datetime
, timedelta
20 from smbus2
import SMBus
22 from smbus
import SMBus
25 def calculate_y_pos(x
, centre
):
26 """Calculates the y-coordinate on a parabolic curve, given x."""
28 y
= 1 / centre
* (x
- centre
) ** 2
33 def circle_coordinates(x
, y
, radius
):
34 """Calculates the bounds of a circle, given centre and radius."""
36 x1
= x
- radius
# Left
37 x2
= x
+ radius
# Right
38 y1
= y
- radius
# Bottom
41 return (x1
, y1
, x2
, y2
)
44 def 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."""
49 start_hue
= start_hue
/ 360 # Rescale to between 0 and 1
50 end_hue
= end_hue
/ 360
54 # Dim the brightness as you move from the centre to the edges
55 val
= 1 - (abs(centre
- x
) / (2 * centre
))
57 # Ramp up towards centre, then back down
62 hue
= start_hue
+ ((x
/ centre
) * (end_hue
- start_hue
))
64 # At night, move towards purple/blue hues and reverse dimming
69 r
, g
, b
= [int(c
* 255) for c
in colorsys
.hsv_to_rgb(hue
, sat
, val
)]
74 def x_from_sun_moon_time(progress
, period
, x_range
):
75 """Recalculate/rescale an amount of progress through a time period."""
77 x
= int((progress
/ period
) * x_range
)
82 def 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'."""
86 city
= lookup(city_name
, database())
88 # Datetime objects for yesterday, today, tomorrow
90 dt
= pytz
.timezone(time_zone
).localize(dt
)
91 yesterday
= today
- timedelta(1)
92 tomorrow
= today
+ timedelta(1)
94 # Sun objects for yesterfay, today, tomorrow
95 sun_yesterday
= sun(city
.observer
, date
=yesterday
)
96 sun_today
= sun(city
.observer
, date
=today
)
97 sun_tomorrow
= sun(city
.observer
, date
=tomorrow
)
99 # Work out sunset yesterday, sunrise/sunset today, and sunrise tomorrow
100 sunset_yesterday
= sun_yesterday
["sunset"]
101 sunrise_today
= sun_today
["sunrise"]
102 sunset_today
= sun_today
["sunset"]
103 sunrise_tomorrow
= sun_tomorrow
["sunrise"]
105 # Work out lengths of day or night period and progress through period
106 if sunrise_today
< dt
< sunset_today
:
108 period
= sunset_today
- sunrise_today
109 mid
= sunrise_today
+ (period
/ 2)
110 progress
= dt
- sunrise_today
112 elif dt
> sunset_today
:
114 period
= sunrise_tomorrow
- sunset_today
115 mid
= sunset_today
+ (period
/ 2)
116 progress
= dt
- sunset_today
120 period
= sunrise_today
- sunset_yesterday
121 mid
= sunset_yesterday
+ (period
/ 2)
122 progress
= dt
- sunset_yesterday
124 # Convert time deltas to seconds
125 progress
= progress
.total_seconds()
126 period
= period
.total_seconds()
128 return (progress
, period
, day
)
131 def 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."""
135 # x-coordinate for sun/moon
136 x
= x_from_sun_moon_time(progress
, period
, WIDTH
)
138 # If it's day, then move right to left
142 # Calculate position on sun/moon's curve
144 y
= calculate_y_pos(x
, centre
)
147 background
= map_colour(x
, 80, mid_hue
, day_hue
, day
)
149 # New image for background colour
150 img
= Image
.new('RGBA', (WIDTH
, HEIGHT
), color
=background
)
151 draw
= ImageDraw
.Draw(img
)
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
)
158 circle
= circle_coordinates(x
, y
, sun_radius
)
159 overlay_draw
.ellipse(circle
, fill
=(200, 200, 50, opacity
))
161 # Overlay the sun/moon on the background as an alpha matte
162 composite
= Image
.alpha_composite(img
, overlay
).filter(ImageFilter
.GaussianBlur(radius
=blur
))
167 def overlay_text(img
, position
, text
, font
, align_right
=False, rectangle
=False):
168 draw
= ImageDraw
.Draw(img
)
169 w
, h
= font
.getsize(text
)
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
)
186 draw
.text(position
, text
, font
=font
, fill
=(255, 255, 255))
190 def get_cpu_temperature():
191 with
open("/sys/class/thermal/thermal_zone0/temp", "r") as f
:
193 temp
= int(temp
) / 1000.0
197 def 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
)
203 def 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
]
209 # Calculate line of best fit
210 line
= numpy
.polyfit(time_vals
, pressure_vals
, 1, full
=True)
212 # Calculate slope, variance, and confidence
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
219 # Calculate change in pressure per hour
220 change_per_hour
= slope
* 60 * 60
221 variance_per_hour
= variance
* 60 * 60
223 mean_pressure
= numpy
.mean(pressure_vals
)
227 if change_per_hour
> 0.5:
229 elif change_per_hour
< -0.5:
231 elif -0.5 <= change_per_hour
<= 0.5:
235 if abs(change_per_hour
) > 3:
238 pressure_vals
.append(pressure
)
240 mean_pressure
= numpy
.mean(pressure_vals
)
244 # time.sleep(interval)
246 return (mean_pressure
, change_per_hour
, trend
)
248 def describe_pressure(pressure
):
249 """Convert pressure into barometer-type description."""
251 description
= "storm"
252 elif 970 <= pressure
< 990:
254 elif 990 <= pressure
< 1010:
255 description
= "change"
256 elif 1010 <= pressure
< 1030:
258 elif pressure
>= 1030:
265 def describe_humidity(humidity
):
266 """Convert relative humidity into good/bad description."""
267 if 40 < humidity
< 60:
274 def describe_light(light
):
275 """Convert light level in lux to descriptive value."""
278 elif 50 <= light
< 100:
280 elif 100 <= light
< 500:
281 description
= "light"
283 description
= "bright"
288 disp
= ST7735
.ST7735(
294 spi_speed_hz
=10000000
302 # The city and timezone that you want to display.
303 city_name
= "Sheffield"
304 time_zone
= "Europe/London"
306 # Values that alter the look of the background
316 font_sm
= ImageFont
.truetype(UserFont
, 12)
317 font_lg
= ImageFont
.truetype(UserFont
, 14)
324 # Set up BME280 weather sensor
326 bme280
= BME280(i2c_dev
=bus
)
332 cpu_temps
= [get_cpu_temperature()] * 5
334 # Set up light sensor
344 # Keep track of time elapsed
345 start_time
= time
.time()
348 path
= os
.path
.dirname(os
.path
.realpath(__file__
))
350 progress
, period
, day
= sun_moon_time(dt
, city_name
, time_zone
)
351 background
= draw_background(progress
, period
, day
)
354 time_elapsed
= time
.time() - start_time
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)
361 temperature
= bme280
.get_temperature()
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
)
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
376 min_temp
= corr_temperature
377 max_temp
= corr_temperature
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
382 if min_temp
is not None and max_temp
is not None:
383 range_string
= f
"{min_temp:.0f}-{max_temp:.0f}"
385 range_string
= "------"
386 img
= overlay_text(img
, (68, 18 + spacing
), range_string
, font_sm
, align_right
=True, rectangle
=True)
387 temp_icon
= Image
.open(path
+ "/icons/temperature.png")
388 img
.paste(temp_icon
, (margin
, 18), mask
=temp_icon
)
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)
398 humidity_icon
= Image
.open(path
+ "/icons/humidity-" + humidity_desc
.lower() + ".png")
399 img
.paste(humidity_icon
, (margin
, 48), mask
=humidity_icon
)
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)
408 light_icon
= Image
.open(path
+ "/icons/bulb-" + light_desc
.lower() + ".png")
409 img
.paste(humidity_icon
, (80, 18), mask
=light_icon
)
412 pressure
= bme280
.get_pressure()
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)
420 pressure_icon
= Image
.open(path
+ "/icons/weather-" + pressure_desc
.lower() + ".png")
421 img
.paste(pressure_icon
, (80, 48), mask
=pressure_icon
)