8913cef932c24dda27d91e6cfe62527dcfcc0a9e
8 import schedule
# https://pypi.org/project/schedule/ via https://stackoverflow.com/a/16786600
9 import datetime
# http://stackoverflow.com/questions/2150739/ddg#28147286
11 # Transitional fix for breaking change in LTR559
12 from ltr559
import LTR559
17 from bme280
import BME280
18 from enviroplus
import gas
19 from subprocess
import PIPE
, Popen
21 from PIL
import ImageDraw
22 from PIL
import ImageFont
23 from fonts
.ttf
import RobotoMedium
as UserFont
28 format
='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s',
30 datefmt
='%Y-%m-%d %H:%M:%S')
32 logging
.info("""all-in-one.py - Displays readings from all of Enviro plus' sensors
37 varLenBufferTTL
= int((2*24*60*60)*10**9) # time-to-live in nanoseconds
39 # BME280 temperature/pressure/humidity sensor
42 # Create ST7735 LCD display class
43 st7735
= ST7735
.ST7735(
49 #rotation=90, # flip upside down wrt enviro+ default orientation
57 HEIGHT
= st7735
.height
59 # Set up canvas and font
60 img
= Image
.new('RGB', (WIDTH
, HEIGHT
), color
=(0, 0, 0))
61 draw
= ImageDraw
.Draw(img
)
62 path
= os
.path
.dirname(os
.path
.realpath(__file__
))
64 font
= ImageFont
.truetype(UserFont
, font_size
)
66 font2
= ImageFont
.truetype(UserFont
, font2_size
)
70 # The position of the top bar
74 # Displays data and text on the 0.96" LCD
75 def display_text(variable
, data
, unit
):
76 # Maintain length of list
77 values
[variable
] = values
[variable
][1:] + [data
]
78 # Scale the values for the variable between 0 and 1
79 vmin
= min(values
[variable
])
80 vmax
= max(values
[variable
])
81 colours
= [(v
- vmin
+ 1) / (vmax
- vmin
+ 1) for v
in values
[variable
]]
82 # Format the variable name and value
83 message
= "{}: {:.1f} {}".format(variable
[:4], data
, unit
)
85 draw
.rectangle((0, 0, WIDTH
, HEIGHT
), (255, 255, 255))
86 for i
in range(len(colours
)):
87 # Convert the values to colours from red to blue
88 colour
= (1.0 - colours
[i
]) * 0.6
89 r
, g
, b
= [int(x
* 255.0) for x
in colorsys
.hsv_to_rgb(colour
, 1.0, 1.0)]
90 # Draw a 1-pixel wide rectangle of colour
91 draw
.rectangle((i
, top_pos
, i
+ 1, HEIGHT
), (r
, g
, b
))
92 # Draw a line graph in black
93 line_y
= HEIGHT
- (top_pos
+ (colours
[i
] * (HEIGHT
- top_pos
))) + top_pos
94 draw
.rectangle((i
, line_y
, i
+ 1, line_y
+ 1), (0, 0, 0))
95 # Write the text at the top in black
96 draw
.text((0, 0), message
, font
=font
, fill
=(0, 0, 0))
99 # Displays data and text on the 0.96" LCD
100 def display_text2(variable
, data
, unit
, values
):
101 # Scale the values for the variable between 0 and 1
102 print('DEBUG:len(values[' + str(variable
) + ']):' + str(len(values
[variable
])))
103 #print('DEBUG:values[' + str(variable) + ']:' + str(values[variable]))
104 vmin
= min(values
[variable
])
105 vmax
= max(values
[variable
])
106 colours
= [(v
- vmin
+ 1) / (vmax
- vmin
+ 1) for v
in values
[variable
]]
107 # Format the variable name and value
108 message
= "{}: {:.1f} {}".format(variable
[:4], data
, unit
)
109 #message = "{}: {:.1f} {}".format(variable[:4], values[variable][-1], unit)
110 logging
.info(message
)
111 draw
.rectangle((0, 0, WIDTH
, HEIGHT
), (255, 255, 255))
112 for i
in range(len(colours
)):
113 # Convert the values to colours from red to blue
114 colour
= (1.0 - colours
[i
]) * 0.6
115 r
, g
, b
= [int(x
* 255.0) for x
in colorsys
.hsv_to_rgb(colour
, 1.0, 1.0)]
116 # Draw a 1-pixel wide rectangle of colour
117 draw
.rectangle((i
, top_pos
, i
+ 1, HEIGHT
), (r
, g
, b
))
118 # Draw a line graph in black
119 line_y
= HEIGHT
- (top_pos
+ (colours
[i
] * (HEIGHT
- top_pos
))) + top_pos
120 draw
.rectangle((i
, line_y
, i
+ 1, line_y
+ 1), (0, 0, 0))
121 # Write the text at the top in black
122 draw
.text((0, 0), message
, font
=font
, fill
=(0, 0, 0))
124 maxMsg
= "MAX:{:.1f}".format(vmax
)
125 durMsg
= "HR:{:.1f}".format(span_time_h
)
126 minMsg
= "MIN:{:.1f}".format(vmin
)
127 maxMsg_y
= int( ((HEIGHT
- top_pos
)/(HEIGHT
))*(HEIGHT
*(1/4)) + top_pos
- (font2_size
/2) )
128 durMsg_y
= int( ((HEIGHT
- top_pos
)/(HEIGHT
))*(HEIGHT
*(2/4)) + top_pos
- (font2_size
/2) )
129 minMsg_y
= int( ((HEIGHT
- top_pos
)/(HEIGHT
))*(HEIGHT
*(3/4)) + top_pos
- (font2_size
/2) )
130 maxMsg_x
= int( WIDTH
*(3/100) )
131 durMsg_x
= int( WIDTH
*(3/100) )
132 minMsg_x
= int( WIDTH
*(3/100) )
133 draw
.text((maxMsg_x
, maxMsg_y
), maxMsg
, font
=font2
, fill
=(0, 0, 0))
134 draw
.text((durMsg_x
, durMsg_y
), durMsg
, font
=font2
, fill
=(0, 0, 0))
135 draw
.text((minMsg_x
, minMsg_y
), minMsg
, font
=font2
, fill
=(0, 0, 0))
139 # Get the temperature of the CPU for compensation
140 def get_cpu_temperature():
141 process
= Popen(['vcgencmd', 'measure_temp'], stdout
=PIPE
, universal_newlines
=True)
142 output
, _error
= process
.communicate()
143 return float(output
[output
.index('=') + 1:output
.rindex("'")])
145 def rel_to_abs(T
,P
,RH
):
146 """Returns absolute humidity given relative humidity.
151 Absolute temperature in units Kelvin (K).
153 Total pressure in units Pascals (Pa).
155 Relative humidity in units percent (%).
159 absolute_humidity : float
160 Absolute humidity in units [kg water vapor / kg dry air].
164 1. Sonntag, D. "Advancements in the field of hygrometry". 1994. https://doi.org/10.1127/metz/3/1994/51
165 2. Green, D. "Perry's Chemical Engineers' Handbook" (8th Edition). Page "12-4". McGraw-Hill Professional Publishing. 2007.
168 Author: Steven Baltakatei Sandoval
180 # print('DEBUG:Input Temperature (K) :' + str(T));
181 # print('DEBUG:Input Pressure (Pa) :' + str(P));
182 # print('DEBUG:Input Rel. Humidity (%) :' + str(RH));
184 # Set constants and initial conversions
185 epsilon
= 0.62198 # (molar mass of water vapor) / (molar mass of dry air)
186 t
= T
- 273.15; # Celsius from Kelvin
187 P_hpa
= P
/ 100; # hectoPascals (hPa) from Pascals (Pa)
189 # Calculate e_w(T), saturation vapor pressure of water in a pure phase, in Pascals
190 ln_e_w
= -6096*T
**-1 + 21.2409642 - 2.711193*10**-2*T
+ 1.673952*10**-5*T
**2 + 2.433502*math
.log(T
); # Sonntag-1994 eq 7; e_w in Pascals
191 e_w
= math
.exp(ln_e_w
);
192 e_w_hpa
= e_w
/ 100; # also save e_w in hectoPascals (hPa)
193 # print('DEBUG:ln_e_w:' + str(ln_e_w)); # debug
194 # print('DEBUG:e_w:' + str(e_w)); # debug
196 # Calculate f_w(P,T), enhancement factor for water
197 f_w
= 1 + (10**-4*e_w_hpa
)/(273 + t
)*(((38 + 173*math
.exp(-t
/43))*(1 - (e_w_hpa
/ P_hpa
))) + ((6.39 + 4.28*math
.exp(-t
/ 107))*((P_hpa
/ e_w_hpa
) - 1))); # Sonntag-1994 eq 22.
198 # print('DEBUG:f_w:' + str(f_w)); # debug
200 # Calculate e_prime_w(P,T), saturation vapor pressure of water in air-water mixture, in Pascals
201 e_prime_w
= f_w
* e_w
; # Sonntag-1994 eq 18
202 # print('DEBUG:e_prime_w:' + str(e_prime_w)); # debug
204 # Calculate e_prime, vapor pressure of water in air, in Pascals
205 e_prime
= (RH
/ 100) * e_prime_w
;
206 # print('DEBUG:e_prime:' + str(e_prime)); # debug
208 # Calculate r, the absolute humidity, in [kg water vapor / kg dry air]
209 r
= (epsilon
* e_prime
) / (P
- e_prime
);
210 # print('DEBUG:r:' + str(r)); # debug
215 # Tuning factor for compensation. Decrease this number to adjust the
216 # temperature down, and increase to adjust up
219 cpu_temps
= [get_cpu_temperature()] * 5
221 delay
= 0.5 # Debounce the proximity tap
222 mode
= 0 # The starting mode
226 # Create a values dict to store the data
227 variables
= ["temperature",
232 values
= {} # Initialize values dictionary
234 values
[v
] = [1] * WIDTH
# Init a WIDTH-length list as value for each string in variables
236 # Create a varLenBuffer dict to store recent data
239 varLenBuffer
[v
] = [] # Init an empty list for each string in variables
241 # Create a varLenBufferFlt dict to store recent data as floats only
244 varLenBufferFlt
[v
] = [] # Init an empty list for each string in variables
246 # Create a fixLenBuffer dict to store data for displaying
249 fixLenBuffer
[v
] = [] # Init an empty list for each string in variables
254 # Desc: Update variables containing latest sensor tuples
255 # Output: (time [ns], unit, float)
256 # now_temp_tuple (°C)
257 # now_pressure_tuple (hPa)
258 # now_humidity_tuple (%)
259 # now_humidity_abs_tuple (g water vapor / kg dry air)
260 # now_illuminance_tuple (lux)
261 # Depends: time, bme280, ltr559, get_cpu_temperature(), rel_to_abs()
263 # Tell function to modify these global variables
264 global now_temp_tuple
265 global now_pressure_tuple
266 global now_humidity_tuple
267 global now_humidity_abs_tuple
268 global now_illuminance_tuple
271 poll_time_ns_start
= time
.time_ns() # Get time reading (unix spoech, nanoseconds)
272 # Get temperature reading
273 cpu_temp
= get_cpu_temperature() # get °C from CPU
274 # Smooth out with some averaging to decrease jitter
275 cpu_temps
= cpu_temps
[1:] + [cpu_temp
]
276 avg_cpu_temp
= sum(cpu_temps
) / float(len(cpu_temps
))
277 raw_temp
= bme280
.get_temperature() # get °C from BME280 sensor
278 now_temp
= raw_temp
- ((avg_cpu_temp
- raw_temp
) / factor
)
279 now_temp_tuple
= (time
.time_ns(), '°C', now_temp
)
280 # Get pressure reading
281 now_time_ns
= time
.time_ns() # Get time reading (unix epoch, nanoseconds)
282 now_pressure
= bme280
.get_pressure() # get hPa from BME280 sensor
283 now_pressure_tuple
= (time
.time_ns(), 'hPa', now_pressure
)
284 # Get relative humidity reading
285 now_humidity
= bme280
.get_humidity() # get % relative humidity from BME280 sensor
286 now_humidity_tuple
= (time
.time_ns(), '%', now_humidity
)
287 # Calculate absolute humidity reading
288 raw_temp_k
= 273.15 + raw_temp
; # convert sensor temp from degC to K
289 now_pressure_pa
= now_pressure
* 100; # convert sensor pressure from hPa to Pa
290 now_humidity_abs
= rel_to_abs(raw_temp_k
,now_pressure_pa
,now_humidity
); # calc abs humidity
291 now_humidity_abs_tuple
= (time
.time_ns(), 'g/kg', now_humidity_abs
);
293 proximity
= ltr559
.get_proximity() # get proximity reading
295 now_illuminance
= ltr559
.get_lux() # get lux reading from LTR-559 sensor if nothing is nearby
296 now_illuminance_tuple
= (time
.time_ns(), 'lux', now_illuminance
)
297 poll_time_ns_end
= time
.time_ns()
298 #print('DEBUG:poll time (s):' + str((poll_time_ns_end - poll_time_ns_start)/1000000000))
301 def dateIso8601Str():
302 nowUTC
= datetime
.datetime
.utcnow().replace(tzinfo
=datetime
.timezone
.utc
).isoformat()
305 def medianSubset(listIn
: list = [], listOutLen
: int = 0) -> list:
306 # Input: int: listOutLen: quantity of elements in output list
307 # list: listIn: input list consisting of integers or floats
308 # Output: list: ints/floats of specified size
309 # Ref/Attrib: PEP 3107 typing https://stackoverflow.com/a/21384492
311 #print('DEBUG:listOutLen:' + str(listOutLen))
312 #print('DEBUG:listIn:' + str(listIn))
314 # Exit for invalid input
315 if not isinstance(listOutLen
, int):
316 raise ValueError('ERROR:Not a valid int:' + str(listOutLen
))
318 if not listOutLen
> 0:
319 raise ValueError('ERROR:Invalid value:' + str(listOutLen
))
320 if not isinstance(listIn
, list):
321 raise ValueError('ERROR:Not a valid list:' + str(listOutLen
))
322 if not all([( (isinstance(x
,int)) or (isinstance(x
,float)) ) for x
in listIn
]):
323 raise ValueError('ERROR:Input list contains something besides integers or floating point numbers.')
326 listOut
= [None] * listOutLen
327 #print('DEBUG:listOut:' + str(listOut))
330 listInLen
= len(listIn
)
331 #print('DEBUG:listInLen:' + str(listInLen))
333 # Calc subset length float
334 subsetLenFloat
= ( (max([listInLen
,listOutLen
]) - 1) /min([listInLen
,listOutLen
]))
335 subsetIndRatio
= ( (listInLen
)/(listOutLen
) )
336 #print('DEBUG:subsetLenFloat: %.5f' % subsetLenFloat)
337 #print('DEBUG:subsetLenFloat2: %.5f' % subsetIndRatio)
339 # Iterate for each element in listOut
340 for i_out
in range(listOutLen
):
341 #print('DEBUG:i_out:' + str(i_out))
342 ## Decide to expand or reduce listIn to produce listOut
343 if listInLen
> listOutLen
:
344 ### reduce listIn to listOut
345 #print('DEBUG:listOutLen:' + str(listOutLen))
346 #print('DEBUG:listInLen:' + str(listInLen))
348 #### Initialize subsetIndLo in first loop
350 #print('DEBUG:subsetIndLo:' + str(subsetIndLo))
351 #print('DEBUG:i_out:' + str(i_out))
352 #### Calc indices of i_out'th subset of listIn
353 subsetIndHi
= (listInLen
- 1) * (i_out
+ 1) // listOutLen
354 subsetLen
= subsetIndHi
- subsetIndLo
+ 1
355 #print('DEBUG:subsetIndLo:' + str(subsetIndLo))
356 #print('DEBUG:subsetIndHi:' + str(subsetIndHi))
357 #print('DEBUG:subsetLen:' + str(subsetLen))
358 #### Extract subset from listIn using indices inclusively
359 subset
= listIn
[ int(subsetIndLo
) : int(subsetIndHi
)+1 ]
360 #print('DEBUG:subset:' + str(subset))
361 #### Calculate median for subset
362 subsetMedian
= np
.median(subset
)
363 #print('DEBUG:subset median:' + str(subsetMedian))
364 #### Set listOut element
365 listOut
[i_out
] = subsetMedian
367 ##### Update subsetIndLo for next loop
368 subsetIndLo
= subsetIndHi
+ 1
369 #print('DEBUG:Updated subsetIndLo:' + str(subsetIndLo))
370 elif listOutLen
> listInLen
:
371 ### Expand listIn to listOut
372 #print('DEBUG:listOutLen:' + str(listOutLen))
373 #print('DEBUG:listInLen:' + str(listInLen))
374 #### Identify index list of lists mapping listIn to ListOut
375 expandIndex
= int(i_out
/ subsetLenFloat
)
376 expandIndex
= min([expandIndex
,(listInLen
- 1)])
377 #print('DEBUG:expandIndex:' + str(expandIndex))
378 listOut
[i_out
] = listIn
[expandIndex
]
379 #print('DEBUG:listOut[i_out]:' + str(listOut[i_out]))
380 elif listOutLen
== listInLen
:
382 #print('DEBUG:end for loop===========')
386 global now_temp_tuple
387 global now_pressure_tuple
388 global now_humidity_tuple
389 global now_humidity_abs_tuple
390 global now_illuminance_tuple
393 global fixLenBufferFlt
395 #print('DEBUG:This is the updateBuffer() function.')
396 #print('DEBUG:===========================================================')
397 #print('DEBUG:===========================================================')
398 # Capture new sensor tuples
400 #print('DEBUG:now_temp_tuple:' + str(now_temp_tuple))
401 #print('DEBUG:now_pressure_tuple:' + str(now_pressure_tuple))
402 #print('DEBUG:now_humidity_tuple:' + str(now_humidity_tuple))
403 #print('DEBUG:now_humidity_abs_tuple:' + str(now_humidity_abs_tuple))
404 #print('DEBUG:now_illuminance_tuple:' + str(now_illuminance_tuple))
406 # Append new sensor tuples to varying-length buffer
408 varLenBuffer
[variables
[0]].append(now_temp_tuple
)
410 varLenBuffer
[variables
[1]].append(now_pressure_tuple
)
412 varLenBuffer
[variables
[2]].append(now_humidity_tuple
)
414 varLenBuffer
[variables
[3]].append(now_humidity_abs_tuple
)
416 varLenBuffer
[variables
[4]].append(now_illuminance_tuple
)
417 #print('DEBUG:varLenBuffer:' + str(varLenBuffer))
419 # Trim outdated sensor tuples from varying-length buffer
420 ## iterate through each tuple list and remove old tuples
421 varLenBufferTemp
= []
423 #varLenBufferTemp = varLenBuffer[v].copy()
424 now_time_ns
= time
.time_ns() # get ns timestamp of now
425 thn_time_ns
= varLenBuffer
[v
][0][0] # get ns timestamp of earliest tuple
426 dif_time_ns
= now_time_ns
- thn_time_ns
# calc nanosecond difference
427 #print('DEBUG:varLenBufferTTL:' + str(varLenBufferTTL))
428 #print('DEBUG:now:' + str(now_time_ns))
429 #print('DEBUG:thn:' + str(thn_time_ns))
430 #print('DEBUG:dif:' + str(dif_time_ns))
431 #print('DEBUG:dif(s):' + str(dif_time_ns / 1000000000))
432 if dif_time_ns
> varLenBufferTTL
:
433 varLenBuffer
[v
].pop(0) # discard earliest tuple if age > varLenBufferTTL
434 print('DEBUG:Len of varLenBuffer[' + str(v
) + ']:' + str(len(varLenBuffer
[v
])))
435 #print('DEBUG:*******************************************')
436 #print('DEBUG:varLenBuffer[variables[' + str(v) + ']]:' + str(varLenBuffer[v]))
437 #print('DEBUG:*******************************************')
439 # Calculate buffer time span in hours
440 ## Get earliest timestamp (use temperature tuples)
441 first_time_ns
= varLenBuffer
[variables
[0]][0][0]
442 last_time_ns
= varLenBuffer
[variables
[0]][-1][0]
443 span_time_ns
= int(last_time_ns
- first_time_ns
)
444 span_time_h
= float(span_time_ns
/ (10**9*60*60)) # nanoseconds to hours
446 # Convert tuple buffer into float buffer
448 varLenBufferFlt
[v
].clear() # clear old float list
449 #print('DEBUG:v:' + str(v))
450 for t
in varLenBuffer
[v
]:
451 #print('DEBUG:t:' + str(t))
452 #print('DEBUG:t[2]:' + str(t[2]))
453 #print('DEBUG:------------------------------------------')
454 #print('DEBUG:varLenBufferFlt[' + str(v) + ']:' + str(varLenBufferFlt[v]))
455 #print('DEBUG:------------------------------------------')
456 if isinstance(t
[2], float):
457 varLenBufferFlt
[v
].append(float(t
[2])) # build new float list
459 varLenBufferFlt
[v
].append(float(-273)) # add obvious zero otherwise
460 #print('DEBUG:varLenBufferFlt[' + str(v) + ']:' + str(varLenBufferFlt[v]))
462 # Compress/expand buffer to fixed-length buffer
464 #print('DEBUG:varLenBufferFlt[0]:' + str(varLenBufferFlt[variables[0]]))
465 fixLenBuffer
[v
] = medianSubset(varLenBufferFlt
[v
], WIDTH
)
466 print('DEBUG:Len of fixLenBuffer[' + str(v
) + ']:' + str(len(fixLenBuffer
[v
])))
467 #print('DEBUG:fixLenBuffer[' + str(v) + ']:' + str(fixLenBuffer[v]))
473 # schedule.every(1).second.do(updateBuffer)
474 schedule
.every().minute
.at(":00").do(updateBuffer
)
475 # schedule.every().minute.at(":05").do(updateBuffer)
476 # schedule.every().minute.at(":10").do(updateBuffer)
477 # schedule.every().minute.at(":15").do(updateBuffer)
478 # schedule.every().minute.at(":20").do(updateBuffer)
479 # schedule.every().minute.at(":25").do(updateBuffer)
480 # schedule.every().minute.at(":30").do(updateBuffer)
481 # schedule.every().minute.at(":35").do(updateBuffer)
482 # schedule.every().minute.at(":40").do(updateBuffer)
483 # schedule.every().minute.at(":45").do(updateBuffer)
484 # schedule.every().minute.at(":50").do(updateBuffer)
485 # schedule.every().minute.at(":55").do(updateBuffer)
486 pollSensors() # initial run to start up sensors
487 time
.sleep(1) # pause to give sensors time to initialize
488 updateBuffer() # initial run
491 proximity
= ltr559
.get_proximity()
493 # If the proximity crosses the threshold, toggle the display mode
494 if proximity
> 1500 and time
.time() - last_page
> delay
:
496 mode
%= len(variables
)
497 last_page
= time
.time()
499 # Run scheduled tasks
500 schedule
.run_pending()
502 # One display mode for each variable
504 # variable = "temperature"
505 ## run function to display latest temp values in variables[mode] list
507 cpu_temp
= get_cpu_temperature()
508 # Smooth out with some averaging to decrease jitter
509 cpu_temps
= cpu_temps
[1:] + [cpu_temp
]
510 avg_cpu_temp
= sum(cpu_temps
) / float(len(cpu_temps
))
511 raw_temp
= bme280
.get_temperature()
512 data
= raw_temp
- ((avg_cpu_temp
- raw_temp
) / factor
)
513 #display_text(variables[mode], data, unit)
514 display_text2(variables
[mode
],data
,unit
,fixLenBuffer
)
517 # variable = "pressure"
519 data
= bme280
.get_pressure()
520 #display_text(variables[mode], data, unit)
521 display_text2(variables
[mode
],data
,unit
,fixLenBuffer
)
524 # variable = "humidity"
526 data
= bme280
.get_humidity()
527 #display_text(variables[mode], data, unit)
528 display_text2(variables
[mode
],data
,unit
,fixLenBuffer
)
531 # variable = "humidity_abs"
533 raw_temp
= bme280
.get_temperature() # get °C from BME280 sensor
534 raw_temp_k
= 273.15 + raw_temp
; # convert sensor temp from degC to K
535 now_pressure
= bme280
.get_pressure() # get hPa from BME280 sensor
536 now_pressure_pa
= now_pressure
* 100; # convert sensor pressure from hPa to Pa
537 now_humidity
= bme280
.get_humidity() # get % relative humidity from BME280 sensor
538 data
= rel_to_abs(raw_temp_k
,now_pressure_pa
,now_humidity
); # calc abs humidity
539 display_text2(variables
[mode
],data
,unit
,fixLenBuffer
)
545 data
= ltr559
.get_lux()
548 #display_text(variables[mode], data, unit)
549 display_text2(variables
[mode
],data
,unit
,fixLenBuffer
)
554 except KeyboardInterrupt: