When I get my new hardware (looks like two-three days from now) I'm going to be adding a BMP180 sensor (barometric pressure/altitude).
I'm going to be replacing this with a DHT22, but that's one LoC change
I’m going to address these bits, but will build upon @alecxe and @ThomasWard answers, so check them out first.
The thing to note is that both the building of your stats and their formatting is not easily extensible to add further hardware. You should try to come up with a more generic approach, mostly using iterator capabilities of the language.
The first thing you’ll need to learn is that, any construct of the form:
def spam():
egg = []
for bacon in stuff:
egg.append(process(bacon))
return egg
can (and must) be replaced by:
a list-comprehension:
def spam():
return [process(bacon) for bacon in stuff]
or a generator:
def spam():
for bacon in stuff:
yield process(bacon)
The generator version does nothing when the function is called but will generate results as needed when the generator (the result of the function call) is iterated over. I’ll use this approach.
A second tool to learn is zip
and tuple unpacking. zip
, when fed with N lists (or iterables, for what its worth) will return a list of N-tuples of the size of the smallest of the input. i.e.:
>>> a = [1, 2, 3, 4]
>>> b = range(10, 5555)
>>> c = ['spam', 'egg', 'bacon']
>>> zip(a, b, c)
[(1, 10, 'spam'), (2, 11, 'egg'), (3, 12, 'bacon')]
This is useful if you have N 2-stats values, to retrieve 2 N-values per stat:
>>> read_1 = [21.42, 56.23]
>>> read_2 = [21.41, 56.12]
>>> read_3 = [21.45, 56.24]
>>> temperatures, humidities = zip(read_1, read_2, read_3)
>>> temperatures
(21.42, 21.41, 21.45)
>>> humidities
(56.23, 56.12, 56.24)
Here temperatures, humidities = zip(...)
is called unpacking. zip
returned a list of 2 elements, each one is being assigned to a different variable. This work because the length of the list is equal to the number of variables to unpack into. It is also possible to use the same approach (with a sligth change of syntax) to use a list of N values as N differents parameters to a function call:
>>> read = [[21.42, 56.23], [21.41, 56.12], [21.45, 56.24]]
>>> temperatures, humidities = zip(*read)
>>> temperatures
(21.42, 21.41, 21.45)
>>> humidities
(56.23, 56.12, 56.24)
Recall that list-comprehension and generator thing? The *param_name
syntax works with both. So it's easy to use a function that build an N×Y iterable of Y sensor values measured N times and turn it into a Y×N list to compute statistics on each N values for each of the Y sensors.
Using the named tuple approach presented by alecxe, we can extend it further (read inherit) to separate even more the concerns of computation vs. representation:
class Stats(namedtuple('Stats', 'average min max')):
def __str__(self):
return '{}, {}, {}'.format(self.average, self.min, self.max)
This way, using str
on a Stats
instance or including it in a format call will return the proper format for each stat.
And, last but not least, the way to make your script more extensible: functools.partial
. This allows you to "preconfigure" a function call with some arguments. So, for instance, Adafruit_DHT.read_retry(Adafruit_DHT.DHT11, 4)
can be called in a two-way process using partial
with: reader = partial(Adafruit_DHT.read_retry, Adafruit_DHT.DHT11, 4)
and then reader()
. How is that an improvement? You can pass a list of partial
objects to a main function, each partial
being responsible to call a different hardware. And the function will just blindly iterate over each reader to generate statistics:
#!/usr/bin/python
import datetime
from collections import namedtuple
from functools import partial
import Adafruit_DHT
class Stats(namedtuple('Stats', 'average min max')):
def __str__(self):
return '{}, {}, {}'.format(self.average, self.min, self.max)
def read_sensor(reader, measurements_count):
for _ in range(measurements_count):
yield reader()
def compute_statistics(readers, measurements_count):
for reader in readers:
measurements = read_sensor(reader, measurements_count)
for sensor_values in zip(*measurements):
average = sum(sensor_values) / len(sensor_values)
yield Stats(average, min(sensor_values), max(sensor_values))
def main(readers, measurements_count=5, log_filename='MainLog.txt'):
date = datetime.utcnow()
statistics = compute_statistics(readers, measurements_count)
formated_stats = ' | '.join(str(stat) for stat in statistics)
with open(log_filename, 'a') as log_file:
log_file.write('{:%Y-%m-%d %H:%M:%S}: {}\n'.format(date, formated_stats))
if __name__ == '__main__':
dht = partial(Adafruit_DHT.read_retry, Adafruit_DHT.DHT11, 4)
# bmp = partial(Adafruit_DHT.read_retry, Adafruit_DHT.XXX, XX)
main([dht])
# main([dht, bmp])
Note that I turned the strftime
call into a format string specifier.
You can even turn the building of readers into its own function to name things and make it clearer what the call is supposed to do:
def equipment_reader(equipment_kind, gpio_pin):
return partial(Adafruit_DHT.read_retry, equipment_kind, gpio_pin)
if __name__ == '__main__':
dht = equipment_reader(Adafruit_DHT.DHT11, gpio_pin=4)
main([dht])
Now, I’m not a huge fan of using Python 2 anymore; and since Adafruit got some Python 3 support, you can improve things a bit by turning the list that main
requires into a variable length argument:
def main(*readers, measurements_count=5, log_filename='MainLog.txt'):
...
if __name__ == '__main__':
dht = equipment_reader(Adafruit_DHT.DHT11, gpio_pin=4)
bmp = equipment_reader(XXX, gpio_pin=XX)
main(dht, bmp)
This feels cleaner to me, but it is invalid syntax before Python 3.5.