Adding all in one weather and light display example
[EVA-2020-02-2.git] / examples / combined.py
1 #!/usr/bin/env python
2
3 import time
4 import colorsys
5 import os
6 import sys
7 import ST7735
8 try:
9 # Transitional fix for breaking change in LTR559
10 from ltr559 import LTR559
11 ltr559 = LTR559()
12 except ImportError:
13 import ltr559
14
15 from bme280 import BME280
16 from pms5003 import PMS5003, ReadTimeoutError as pmsReadTimeoutError
17 from enviroplus import gas
18 from subprocess import PIPE, Popen
19 from PIL import Image
20 from PIL import ImageDraw
21 from PIL import ImageFont
22 import logging
23
24 logging.basicConfig(
25 format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s',
26 level=logging.INFO,
27 datefmt='%Y-%m-%d %H:%M:%S')
28
29 logging.info("""all-in-one.py - Displays readings from all of Enviro plus' sensors
30
31 Press Ctrl+C to exit!
32
33 """)
34
35 # BME280 temperature/pressure/humidity sensor
36 bme280 = BME280()
37
38 # PMS5003 particulate sensor
39 pms5003 = PMS5003()
40
41 # Create ST7735 LCD display class
42 st7735 = ST7735.ST7735(
43 port=0,
44 cs=1,
45 dc=9,
46 backlight=12,
47 rotation=270,
48 spi_speed_hz=10000000
49 )
50
51 # Initialize display
52 st7735.begin()
53
54 WIDTH = st7735.width
55 HEIGHT = st7735.height
56
57 # Set up canvas and font
58 img = Image.new('RGB', (WIDTH, HEIGHT), color=(0, 0, 0))
59 draw = ImageDraw.Draw(img)
60 path = os.path.dirname(os.path.realpath(__file__))
61 font = ImageFont.truetype(path + "/fonts/Asap/Asap-Bold.ttf", 20)
62 smallfont = ImageFont.truetype(path + "/fonts/Asap/Asap-Bold.ttf", 10)
63 x_offset = 2
64 y_offset = 2
65
66 message = ""
67
68 # The position of the top bar
69 top_pos = 25
70
71 # Create a values dict to store the data
72 variables = ["temperature",
73 "pressure",
74 "humidity",
75 "light",
76 "oxidised",
77 "reduced",
78 "nh3",
79 "pm1",
80 "pm25",
81 "pm10"]
82
83 units = ["C",
84 "hPa",
85 "%",
86 "Lux",
87 "kO",
88 "kO",
89 "kO",
90 "ug/m3",
91 "ug/m3",
92 "ug/m3"]
93
94 # Define your own warning limits
95 # The limits definition follows the order of the variables array
96 # Example limits explanation for temperature:
97 # [4,18,28,35] means
98 # [-273.15 .. 4] -> Dangerously Low
99 # (4 .. 18] -> Low
100 # (18 .. 28] -> Normal
101 # (28 .. 35] -> High
102 # (35 .. MAX] -> Dangerously High
103 # DISCLAIMER: The limits provided here are just examples and come
104 # with NO WARRANTY. The authors of this example code claim
105 # NO RESPONSIBILITY if reliance on the following values or this
106 # code in general leads to ANY DAMAGES or DEATH.
107 limits = [[4,18,28,35],
108 [250,650,1013.25,1015],
109 [20,30,60,70],
110 [-1,-1,30000,100000],
111 [-1,-1,40,50],
112 [-1,-1,450,550],
113 [-1,-1,200,300],
114 [-1,-1,50,100],
115 [-1,-1,50,100],
116 [-1,-1,50,100]]
117
118 # RGB palette for values on the combined screen
119 palette = [(0,0,255), # Dangerously Low
120 (0,255,255), # Low
121 (0,255,0), # Normal
122 (255,255,0), # High
123 (255,0,0)] # Dangerously High
124
125 values = {}
126
127
128 # Displays data and text on the 0.96" LCD
129 def display_text(variable, data, unit):
130 # Maintain length of list
131 values[variable] = values[variable][1:] + [data]
132 # Scale the values for the variable between 0 and 1
133 colours = [(v - min(values[variable]) + 1) / (max(values[variable])
134 - min(values[variable]) + 1) for v in values[variable]]
135 # Format the variable name and value
136 message = "{}: {:.1f} {}".format(variable[:4], data, unit)
137 logging.info(message)
138 draw.rectangle((0, 0, WIDTH, HEIGHT), (255, 255, 255))
139 for i in range(len(colours)):
140 # Convert the values to colours from red to blue
141 colour = (1.0 - colours[i]) * 0.6
142 r, g, b = [int(x * 255.0) for x in colorsys.hsv_to_rgb(colour,
143 1.0, 1.0)]
144 # Draw a 1-pixel wide rectangle of colour
145 draw.rectangle((i, top_pos, i+1, HEIGHT), (r, g, b))
146 # Draw a line graph in black
147 line_y = HEIGHT - (top_pos + (colours[i] * (HEIGHT - top_pos)))\
148 + top_pos
149 draw.rectangle((i, line_y, i+1, line_y+1), (0, 0, 0))
150 # Write the text at the top in black
151 draw.text((0, 0), message, font=font, fill=(0, 0, 0))
152 st7735.display(img)
153
154 # Saves the data to be used in the graphs later and prints to the log
155 def save_data(idx, data):
156 variable = variables[idx]
157 # Maintain length of list
158 values[variable] = values[variable][1:] + [data]
159 unit = units[idx]
160 message = "{}: {:.1f} {}".format(variable[:4], data, unit)
161 logging.info(message)
162
163
164 # Displays all the text on the 0.96" LCD
165 def display_everything():
166 draw.rectangle((0, 0, WIDTH, HEIGHT), (0, 0, 0))
167 column_count = 2
168 row_count = (len(variables)/column_count)
169 for i in xrange(len(variables)):
170 variable = variables[i]
171 data_value = values[variable][-1]
172 unit = units[i]
173 x = x_offset + ((WIDTH/column_count) * (i / row_count))
174 y = y_offset + ((HEIGHT/row_count) * (i % row_count))
175 message = "{}: {:.1f} {}".format(variable[:4], data_value, unit)
176 lim = limits[i]
177 rgb = palette[0]
178 for j in xrange(len(lim)):
179 if data_value > lim[j]:
180 rgb = palette[j+1]
181 draw.text((x, y), message, font=smallfont, fill=rgb)
182 st7735.display(img)
183
184
185
186 # Get the temperature of the CPU for compensation
187 def get_cpu_temperature():
188 process = Popen(['vcgencmd', 'measure_temp'], stdout=PIPE, universal_newlines=True)
189 output, _error = process.communicate()
190 return float(output[output.index('=') + 1:output.rindex("'")])
191
192
193 # Tuning factor for compensation. Decrease this number to adjust the
194 # temperature down, and increase to adjust up
195 factor = 1.95
196
197 cpu_temps = [get_cpu_temperature()] * 5
198
199 delay = 0.5 # Debounce the proximity tap
200 mode = 10 # The starting mode
201 last_page = 0
202 light = 1
203
204 for v in variables:
205 values[v] = [1] * WIDTH
206
207 # The main loop
208 try:
209 while True:
210 proximity = ltr559.get_proximity()
211
212 # If the proximity crosses the threshold, toggle the mode
213 if proximity > 1500 and time.time() - last_page > delay:
214 mode += 1
215 mode %= (len(variables)+1)
216 last_page = time.time()
217
218 # One mode for each variable
219 if mode == 0:
220 # variable = "temperature"
221 unit = "C"
222 cpu_temp = get_cpu_temperature()
223 # Smooth out with some averaging to decrease jitter
224 cpu_temps = cpu_temps[1:] + [cpu_temp]
225 avg_cpu_temp = sum(cpu_temps) / float(len(cpu_temps))
226 raw_temp = bme280.get_temperature()
227 data = raw_temp - ((avg_cpu_temp - raw_temp) / factor)
228 display_text(variables[mode], data, unit)
229
230 if mode == 1:
231 # variable = "pressure"
232 unit = "hPa"
233 data = bme280.get_pressure()
234 display_text(variables[mode], data, unit)
235
236 if mode == 2:
237 # variable = "humidity"
238 unit = "%"
239 data = bme280.get_humidity()
240 display_text(variables[mode], data, unit)
241
242 if mode == 3:
243 # variable = "light"
244 unit = "Lux"
245 if proximity < 10:
246 data = ltr559.get_lux()
247 else:
248 data = 1
249 display_text(variables[mode], data, unit)
250
251 if mode == 4:
252 # variable = "oxidised"
253 unit = "kO"
254 data = gas.read_all()
255 data = data.oxidising / 1000
256 display_text(variables[mode], data, unit)
257
258 if mode == 5:
259 # variable = "reduced"
260 unit = "kO"
261 data = gas.read_all()
262 data = data.reducing / 1000
263 display_text(variables[mode], data, unit)
264
265 if mode == 6:
266 # variable = "nh3"
267 unit = "kO"
268 data = gas.read_all()
269 data = data.nh3 / 1000
270 display_text(variables[mode], data, unit)
271
272 if mode == 7:
273 # variable = "pm1"
274 unit = "ug/m3"
275 try:
276 data = pms5003.read()
277 except pmsReadTimeoutError:
278 logging.warn("Failed to read PMS5003")
279 else:
280 data = float(data.pm_ug_per_m3(1.0))
281 display_text(variables[mode], data, unit)
282
283 if mode == 8:
284 # variable = "pm25"
285 unit = "ug/m3"
286 try:
287 data = pms5003.read()
288 except pmsReadTimeoutError:
289 logging.warn("Failed to read PMS5003")
290 else:
291 data = float(data.pm_ug_per_m3(2.5))
292 display_text(variables[mode], data, unit)
293
294 if mode == 9:
295 # variable = "pm10"
296 unit = "ug/m3"
297 try:
298 data = pms5003.read()
299 except pmsReadTimeoutError:
300 logging.warn("Failed to read PMS5003")
301 else:
302 data = float(data.pm_ug_per_m3(10))
303 display_text(variables[mode], data, unit)
304 if mode == 10:
305 # Everything on one screen
306 cpu_temp = get_cpu_temperature()
307 # Smooth out with some averaging to decrease jitter
308 cpu_temps = cpu_temps[1:] + [cpu_temp]
309 avg_cpu_temp = sum(cpu_temps) / float(len(cpu_temps))
310 raw_temp = bme280.get_temperature()
311 raw_data = raw_temp - ((avg_cpu_temp - raw_temp) / factor)
312 save_data(0, raw_data)
313 display_everything()
314 raw_data = bme280.get_pressure()
315 save_data(1, raw_data)
316 display_everything()
317 raw_data = bme280.get_humidity()
318 save_data(2, raw_data)
319 if proximity < 10:
320 raw_data = ltr559.get_lux()
321 else:
322 raw_data = 1
323 save_data(3, raw_data)
324 display_everything()
325 gas_data = gas.read_all()
326 save_data(4, gas_data.oxidising / 1000)
327 save_data(5, gas_data.reducing / 1000)
328 save_data(6, gas_data.nh3 / 1000)
329 display_everything()
330 pms_data = None
331 try:
332 pms_data = pms5003.read()
333 except pmsReadTimeoutError:
334 logging.warn("Failed to read PMS5003")
335 else:
336 save_data(7, float(pms_data.pm_ug_per_m3(1.0)))
337 save_data(8, float(pms_data.pm_ug_per_m3(2.5)))
338 save_data(9, float(pms_data.pm_ug_per_m3(10)))
339 display_everything()
340
341
342
343 # Exit cleanly
344 except KeyboardInterrupt:
345 sys.exit(0)