-
Notifications
You must be signed in to change notification settings - Fork 2
/
draw_weather.py
318 lines (257 loc) · 13 KB
/
draw_weather.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
import math
from datetime import datetime
from PIL import ImageFont, Image, ImageDraw
from config import workdir, CONFIG, log
from epd_handler import draw_image_on_hardware, screenWidth, screenHeight
from ttf import icon_to_unicode, icon_id_to_unicode_ttf, get_text_size
clockFont = ImageFont.truetype(workdir + "/fonts/Ubuntu Nerd Font Complete.ttf", 28)
dateFont = ImageFont.truetype(workdir + "/fonts/Ubuntu Nerd Font Complete.ttf", 11)
wCityFont = ImageFont.truetype(workdir + "/fonts/Ubuntu Nerd Font Complete.ttf", 11)
wIcoFont = ImageFont.truetype(workdir + "/fonts/weathericons-regular-webfont.ttf", 30)
wDetFont = ImageFont.truetype(workdir + "/fonts/Ubuntu Nerd Font Complete.ttf", 11)
wTempFont = ImageFont.truetype(workdir + "/fonts/Ubuntu Nerd Font Complete.ttf", 20)
wHumFont = ImageFont.truetype(workdir + "/fonts/Ubuntu Nerd Font Complete.ttf", 20)
sIcoFont = ImageFont.truetype(workdir + "/fonts/Ubuntu Nerd Font Complete.ttf", 20)
sTempFont = ImageFont.truetype(workdir + "/fonts/Ubuntu Nerd Font Complete.ttf", 11)
sHumFont = ImageFont.truetype(workdir + "/fonts/Ubuntu Nerd Font Complete.ttf", 11)
fBoxFont = ImageFont.truetype(workdir + "/fonts/Ubuntu Nerd Font Complete.ttf", 11)
fBoxWiFont = ImageFont.truetype(workdir + "/fonts/weathericons-regular-webfont.ttf", 20)
sunTimeFont = ImageFont.truetype(workdir + "/fonts/weathericons-regular-webfont.ttf", 10)
wiXmlMap = workdir + "/fonts/values/weathericons.xml"
black = 'rgb(0,0,0)'
white = 'rgb(255,255,255)'
grey = 'rgb(235,235,235)'
wx0 = 0
hx0 = 0
# 180° rotated display
w_wx0 = 0 # current weather area: start width
w_hx0 = 0 # current weather area: start height
w_wx1 = screenWidth # current weather area: end width
w_hx1 = int(screenHeight / 2) # current weather area: end height
f_wx0 = 0 # forecast area: start width
f_hx0 = w_hx1 # forecast area: start height
f_wx1 = screenWidth # forecast area: end width
f_hx1 = int(screenHeight / 2) # forecast area: end height
forecast_boxes_row = 2
forecast_boxes_col = 4
fBox_wx1 = math.floor(w_wx1 / forecast_boxes_col)
fBox_hx1 = math.floor(f_hx1 / forecast_boxes_row)
def _current_time():
"""
Format current time (hour & minutes)
:return: str
"""
return datetime.now().strftime('%H:%M')
def _current_date():
"""
Format current date
:return: str
"""
return datetime.now().strftime('%a, %b %d')
def draw_image(weather: dict, fcast: dict, temp_and_humidity: (float, float)):
"""
Draw the whole image to display
:param weather: Current weather data
:param fcast: Current forecast data
:param temp_and_humidity: (temperature, humidity)
"""
img = Image.new("L", (screenWidth, screenHeight), 255)
draw = ImageDraw.Draw(img)
draw_upper_part(draw, weather, temp_and_humidity)
draw_forecast_boxes(draw, fcast)
img_rotated = img.transpose(Image.ROTATE_180)
draw_image_on_hardware(img_rotated)
img_rotated.close()
def draw_forecast_boxes(draw: ImageDraw, fcast: dict):
"""
Draw all forecast boxes
:param draw: Image with drawing
:param fcast: Forecast data
"""
for fcast_col in range(forecast_boxes_col):
for fcast_row in range(forecast_boxes_row):
box_pos = (fcast_row * forecast_boxes_col) + fcast_col
slot = box_pos + 1
if slot >= len(fcast["list"]):
break
forecast_data = format_data(fcast, slot)
draw_forecast_box(draw, forecast_data, fcast_col, fcast_row)
def draw_forecast_box(draw: ImageDraw, forecast_data: dict[str, str], box_col: int, box_row: int):
"""
Draw a single forecast box
:param forecast_data: Forecast data
:param box_col: Number of colum
:param box_row: Number of row
:param draw: Image with drawing
"""
# Forecast box boundaries
box_wx0 = f_wx0 + (box_col * fBox_wx1)
box_hx0 = f_hx0 + (box_row * fBox_hx1)
box_wx1 = box_wx0 + fBox_wx1
box_hx1 = box_hx0 + fBox_hx1
draw.rectangle(((box_wx0, box_hx0), (box_wx1, box_hx1)), outline=black, width=1)
draw.line([(box_wx1, box_hx0), (box_wx1, box_hx1)], fill=black, width=1)
draw.line([(box_wx0, box_hx1), (box_wx1, box_hx1)], fill=black, width=1)
# Loading data
weather_icon = icon_id_to_unicode_ttf(forecast_data['iconId'], forecast_data['pod'], wiXmlMap)
hour_text = forecast_data['time']
temperature_text = forecast_data['temp']
# Text size
ico_width, ico_height = get_text_size(draw=draw, text=weather_icon, font=fBoxWiFont)
hour_width, hour_height = get_text_size(draw=draw, text=hour_text, font=fBoxFont)
temp_width, temp_height = get_text_size(draw=draw, text=temperature_text, font=fBoxFont)
width_padding = (fBox_wx1 - (ico_width + temp_width)) / 2
height_padding = (fBox_hx1 - (hour_height + temp_height)) / 2
# Print Weather Icon
ico_rel_wx0 = box_wx0 + int((width_padding / 2))
ico_rel_hx0 = box_hx0
draw.text((ico_rel_wx0, ico_rel_hx0), weather_icon, fill=black, font=fBoxWiFont)
# Print Time
hour_rel_wx0 = ico_rel_wx0 + ico_width + int((width_padding / 2))
hour_rel_hx0 = box_hx0
draw.text((hour_rel_wx0, hour_rel_hx0), hour_text, fill=black, font=fBoxFont)
# Print Temperature
temp_rel_wx0 = ico_rel_wx0 + ico_width + int((width_padding / 2))
temp_rel_hx0 = box_hx0 + hour_height + height_padding
draw.text((temp_rel_wx0, temp_rel_hx0), temperature_text, fill=black, font=fBoxFont)
def draw_local_data(draw: ImageDraw, available_width: int, local_hx0: int, temp_and_humidity: (float, float)):
"""
Draw local data
:param draw: ImageDraw
:param available_width: Available space
:param local_hx0: Position in height
:param temp_and_humidity: (temperature, humidity)
"""
padding = 2
home_icon = '\uf015'
text_temp = str(temp_and_humidity[0]) + "°C"
text_hum = str(temp_and_humidity[1]) + "%"
w_ico, h_ico = get_text_size(draw=draw, text=home_icon, font=sIcoFont)
w_temp, h_temp = get_text_size(draw=draw, text=text_temp, font=wDetFont)
w_hum, h_hum = get_text_size(draw=draw, text=text_hum, font=wDetFont)
width_data = padding + w_ico + 2 + max(w_temp, w_hum)
if available_width >= width_data:
home_wx0 = w_wx1 - width_data
draw.text(xy=(home_wx0, local_hx0), text=home_icon, font=sIcoFont, fill=black)
draw.text(xy=(home_wx0 + 2 + w_ico, local_hx0), text=text_temp, font=wDetFont, fill=black)
draw.text(xy=(home_wx0 + 2 + w_ico, local_hx0 + 2 + h_temp), text=text_hum, font=wDetFont, fill=black)
def draw_upper_part(draw: ImageDraw, weather: dict, temp_and_humidity: (float, float)):
"""
Draw current weather info
:param temp_and_humidity: (temperature, humidity)
:param draw: Pillow Draw
:param weather: Data about weather
"""
left_padding = 5
# Date & Time
_, _, hour_wx1, hour_hx1 = draw.textbbox(xy=(left_padding, 0), text=_current_time(), font=clockFont)
_, _, _, date_hx1 = draw.textbbox(xy=(left_padding, hour_hx1), text=_current_date(),
font=dateFont)
draw.text((left_padding, 0), _current_time(), fill=black, font=clockFont)
draw.text((left_padding, hour_hx1), _current_date(), fill=black, font=dateFont)
# Current weather
city_text = CONFIG.get("city", weather["name"])
temperature_text = weather["main"]["temp"]
temp_feels_text = weather["main"]["feels_like"]
humidity_text = weather["main"]["humidity"]
description_text = weather["weather"][0]["description"]
sunrise_text = datetime.fromtimestamp(weather["sys"]["sunrise"]).strftime('%H:%M')
sunset_text = datetime.fromtimestamp(weather["sys"]["sunset"]).strftime('%H:%M')
weather_ico_code = weather["weather"][0]["icon"]
# Weather Icon
weather_icon_unicode = icon_to_unicode(weather_ico_code, wiXmlMap)
ico_wx0, _, ico_wx1, ico_hx1 = draw.textbbox(xy=(hour_wx1 + left_padding, 0),
text=weather_icon_unicode, font=wIcoFont)
draw.text((ico_wx0, 0), weather_icon_unicode, fill=black, font=wIcoFont)
# City Name
city_and_desc_hx0 = max(ico_hx1, date_hx1) + left_padding
city_wx0, _, _, _ = draw.textbbox(xy=(left_padding, city_and_desc_hx0), text=city_text,
font=wCityFont)
draw.text((city_wx0, city_and_desc_hx0), city_text, fill=black, font=wCityFont)
# Description Weather
desc_width, desc_height = get_text_size(draw=draw, text=description_text, font=wDetFont)
desc_wx0, desc_hx0 = ico_wx0, city_and_desc_hx0
draw.text((desc_wx0, desc_hx0), description_text, fill=black, font=wDetFont)
# Temperature
temperature_text = str(math.ceil(temperature_text)) + "°C"
temperature_wx0, _, temperature_wx1, temperature_hx1 = draw.textbbox(xy=(ico_wx1, 1),
text=temperature_text,
font=wTempFont)
values_wx0 = max(ico_wx1, desc_width)
values_size_available = int((w_wx1 - values_wx0) / 2)
temperature_padding = int(((values_size_available - (temperature_wx1 - temperature_wx0)) / 2))
draw.text((temperature_wx0 + temperature_padding, 1), temperature_text, fill=black, font=wTempFont)
# Humidity
humidity_text = str(humidity_text) + "%"
humidity_wx0, _, humidity_wx1, _ = draw.textbbox(xy=(ico_wx1, temperature_hx1),
text=humidity_text, font=wHumFont)
humidity_padding = int(((values_size_available - (humidity_wx1 - humidity_wx0)) / 2))
draw.text((humidity_wx0 + humidity_padding, temperature_hx1), humidity_text, fill=black, font=wHumFont)
# Feels Temperature
# temp_feels_text = " => " + str(math.ceil(temp_feels_text)) + "°C"
# draw.text((temperature_wx1 + temperature_padding, 1), temp_feels_text, fill=black, font=wTempFont)
# Local Data
available_width = w_wx1 - (temperature_wx0 + temperature_padding)
draw_local_data(draw, available_width, 0, temp_and_humidity)
# Sunrise & Sunset
available_width = w_wx1 - (desc_wx0 + desc_width)
draw_sun_times(draw, available_width, desc_hx0, sunrise_text, sunset_text)
def draw_sun_times(draw: ImageDraw, available_width: int, sun_hx0: int, sunrise_text: str, sunset_text: str):
"""
Draw sunrise & sunset if there is available space
:param draw: ImageDraw
:param available_width: Available width needed for drawing
:param sun_hx0: Height position
:param sunrise_text: Sunrise time
:param sunset_text: Sunset time
"""
sunrise_icon = icon_id_to_unicode_ttf('sunrise', '', wiXmlMap)
sunset_icon = icon_id_to_unicode_ttf('moonrise', '', wiXmlMap)
w_ico_sunrise, _ = get_text_size(draw=draw, text=sunrise_icon, font=sunTimeFont)
w_sunrise, _ = get_text_size(draw=draw, text=sunrise_text, font=wDetFont)
w_ico_sunset, _ = get_text_size(draw=draw, text=sunset_icon, font=wDetFont)
w_sunset, _ = get_text_size(draw=draw, text=sunset_text, font=wDetFont)
len_sun_time = w_ico_sunrise + 2 + w_sunrise + 5 + w_ico_sunset + 2 + w_sunset
if available_width >= len_sun_time:
sun_wx0 = w_wx1 - len_sun_time
draw.text(xy=(sun_wx0, sun_hx0), text=sunrise_icon, font=sunTimeFont, fill=black)
draw.text(xy=(sun_wx0 + w_ico_sunrise + 2, sun_hx0), text=sunrise_text, font=wDetFont, fill=black)
draw.text(xy=(sun_wx0 + w_ico_sunrise + 2 + w_sunrise + 5, sun_hx0), text=sunset_icon, font=sunTimeFont,
fill=black)
draw.text(xy=(sun_wx0 + w_ico_sunrise + 2 + w_sunrise + 5 + w_ico_sunset + 2, sun_hx0),
text=sunset_text, font=wDetFont, fill=black)
def draw_error(exception: Exception, retry_period: str):
"""
Draw an error image
:param exception: Exception
:param retry_period: Next retry time
"""
error_image = Image.new('1', (screenWidth, screenHeight), 255)
draw = ImageDraw.Draw(error_image)
draw.text((0, 0), "ERROR", font=clockFont, fill=black)
draw.text((0, 50), str(exception), font=dateFont, fill=black)
draw.text((0, 70), 'Retrying in ' + retry_period + 's', font=dateFont, fill=black)
draw.text((0, 80), 'Last Refresh: ' + str(_current_time()) + " " + str(_current_date()), font=dateFont, fill=black)
img_rotated = error_image.transpose(Image.ROTATE_180)
draw_image_on_hardware(img_rotated)
img_rotated.close()
def format_data(fcast_json: dict, slot: int) -> dict[str, str]:
"""
Format the data into a dict
:param fcast_json: JSON
:param slot: Slot of array to load data from
:return: Formatted data
"""
data = {}
fcast_data = fcast_json['list'][slot]
temp = math.ceil(fcast_data['main']['temp'])
temp_feel = math.ceil(fcast_data['main']['feels_like'])
hum = math.ceil(fcast_data['main']['humidity'])
dt = datetime.utcfromtimestamp(fcast_data['dt'])
data['temp'] = str(temp) + "°C"
data['temp_feel'] = str(temp_feel) + "°C"
data['hum'] = str(hum) + "%"
data['iconId'] = str(fcast_data['weather'][0]['id'])
data['pod'] = str(fcast_data['sys']['pod'])
data['time'] = dt.strftime("%H:%M")
return data