diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 89154d5..5fac95b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: [2.7, 3.5, 3.6, 3.7, 3.8] + python: [3.6, 3.7, 3.9] steps: - uses: actions/checkout@v2 @@ -33,5 +33,5 @@ jobs: run: | python -m pip install coveralls coveralls --service=github - if: ${{ matrix.python == '3.8' }} + if: ${{ matrix.python == '3.9' }} diff --git a/README.md b/README.md index 4bce5f5..ad90197 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ curl -sSL https://get.pimoroni.com/enviroplus | bash ## Or... Install from PyPi and configure manually: -* Run `sudo pip install enviroplus` +* Run `sudo python3 -m pip install enviroplus` **Note** this wont perform any of the required configuration changes on your Pi, you may additionally need to: diff --git a/examples/luftdaten.py b/examples/luftdaten.py index 0c13771..d0bee37 100755 --- a/examples/luftdaten.py +++ b/examples/luftdaten.py @@ -127,7 +127,7 @@ def send_to_luftdaten(values, id): resp_pm = None resp_bmp = None - + try: resp_pm = requests.post( "https://api.luftdaten.info/v1/push-sensor-data/", @@ -150,7 +150,7 @@ def send_to_luftdaten(values, id): except requests.exceptions.RequestException as e: logging.warning('Luftdaten PM Request Error: {}'.format(e)) - try: + try: resp_bmp = requests.post( "https://api.luftdaten.info/v1/push-sensor-data/", json={ diff --git a/examples/mqtt-all.py b/examples/mqtt-all.py index b725a6c..8220c67 100755 --- a/examples/mqtt-all.py +++ b/examples/mqtt-all.py @@ -1,9 +1,9 @@ +#!/usr/bin/env python3 """ Run mqtt broker on localhost: sudo apt-get install mosquitto mosquitto-clients Example run: python3 mqtt-all.py --broker 192.168.1.164 --topic enviro --username xxx --password xxxx """ -#!/usr/bin/env python3 import argparse import ST7735 @@ -43,6 +43,7 @@ DEFAULT_USERNAME = None DEFAULT_PASSWORD = None + # mqtt callbacks def on_connect(client, userdata, flags, rc): if rc == 0: @@ -99,7 +100,7 @@ def get_cpu_temperature(): ["vcgencmd", "measure_temp"], stdout=PIPE, universal_newlines=True ) output, _error = process.communicate() - return float(output[output.index("=") + 1 : output.rindex("'")]) + return float(output[output.index("=") + 1:output.rindex("'")]) # Get Raspberry Pi serial number to use as ID @@ -240,7 +241,7 @@ def main(): HAS_PMS = False try: pms5003 = PMS5003() - pm_values = pms5003.read() + _ = pms5003.read() HAS_PMS = True print("PMS5003 sensor is connected") except SerialTimeoutError: diff --git a/install.sh b/install.sh index 6837697..a8513b6 100755 --- a/install.sh +++ b/install.sh @@ -139,14 +139,6 @@ printf "$LIBRARY_NAME $LIBRARY_VERSION Python Library: Installer\n\n" cd library -printf "Installing for Python 2..\n" -apt_pkg_install "${PY2_DEPS[@]}" -python setup.py install > /dev/null -if [ $? -eq 0 ]; then - success "Done!\n" - echo "pip uninstall $LIBRARY_NAME" >> $UNINSTALLER -fi - if [ -f "/usr/bin/python3" ]; then printf "Installing for Python 3..\n" apt_pkg_install "${PY3_DEPS[@]}" @@ -155,6 +147,9 @@ if [ -f "/usr/bin/python3" ]; then success "Done!\n" echo "pip3 uninstall $LIBRARY_NAME" >> $UNINSTALLER fi +else + printf "/usr/bin/python3 not found. Unable to install!\n" + exit 1 fi cd $WD diff --git a/library/CHANGELOG.txt b/library/CHANGELOG.txt index 4e59b22..4dd54d5 100644 --- a/library/CHANGELOG.txt +++ b/library/CHANGELOG.txt @@ -1,3 +1,8 @@ +0.0.5 +----- + +* Drop Python 2.x support + 0.0.4 ----- diff --git a/library/README.md b/library/README.md index 43572bf..a1136e8 100644 --- a/library/README.md +++ b/library/README.md @@ -35,7 +35,7 @@ curl -sSL https://get.pimoroni.com/enviroplus | bash ## Or... Install from PyPi and configure manually: -* Run `sudo pip install enviroplus` +* Run `sudo python3 -m pip install enviroplus` **Note** this wont perform any of the required configuration changes on your Pi, you may additionally need to: @@ -70,6 +70,12 @@ sudo apt install python-numpy python-smbus python-pil python-setuptools * Discord - https://discord.gg/hr93ByC # Changelog + +0.0.5 +----- + +* Drop Python 2.x support + 0.0.4 ----- diff --git a/library/enviroplus/__init__.py b/library/enviroplus/__init__.py index 156d6f9..eead319 100644 --- a/library/enviroplus/__init__.py +++ b/library/enviroplus/__init__.py @@ -1 +1 @@ -__version__ = '0.0.4' +__version__ = '0.0.5' diff --git a/library/enviroplus/gas.py b/library/enviroplus/gas.py index f5eb2ab..54c240f 100644 --- a/library/enviroplus/gas.py +++ b/library/enviroplus/gas.py @@ -10,6 +10,7 @@ ads1015.I2C_ADDRESS_DEFAULT = ads1015.I2C_ADDRESS_ALTERNATE _is_setup = False +_is_available = False _adc_enabled = False _adc_gain = 6.148 @@ -41,13 +42,19 @@ def __repr__(self): def setup(): - global adc, adc_type, _is_setup + global adc, adc_type, _is_setup, _is_available if _is_setup: return _is_setup = True - adc = ads1015.ADS1015(i2c_addr=0x49) - adc_type = adc.detect_chip_type() + try: + adc = ads1015.ADS1015(i2c_addr=0x49) + adc_type = adc.detect_chip_type() + _is_available = True + except IOError: + _is_available = False + return + adc.set_mode('single') adc.set_programmable_gain(MICS6814_GAIN) if adc_type == 'ADS1115': @@ -62,6 +69,11 @@ def setup(): atexit.register(cleanup) +def available(): + setup() + return _is_available + + def enable_adc(value=True): """Enable reading from the additional ADC pin.""" global _adc_enabled @@ -81,6 +93,10 @@ def cleanup(): def read_all(): """Return gas resistence for oxidising, reducing and NH3""" setup() + + if not _is_available: + raise RuntimeError("Gas sensor not connected.") + ox = adc.get_voltage('in0/gnd') red = adc.get_voltage('in1/gnd') nh3 = adc.get_voltage('in2/gnd') @@ -119,7 +135,6 @@ def read_oxidising(): Eg chlorine, nitrous oxide """ - setup() return read_all().oxidising @@ -128,17 +143,14 @@ def read_reducing(): Eg hydrogen, carbon monoxide """ - setup() return read_all().reducing def read_nh3(): """Return gas resistance for nh3/ammonia""" - setup() return read_all().nh3 def read_adc(): """Return spare ADC channel value""" - setup() return read_all().adc diff --git a/library/setup.cfg b/library/setup.cfg index a6ba0ba..591de90 100644 --- a/library/setup.cfg +++ b/library/setup.cfg @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- [metadata] name = enviroplus -version = 0.0.4 +version = 0.0.5 author = Philip Howard author_email = phil@pimoroni.com description = Enviro pHAT Plus environmental monitoring add-on for Raspberry Pi @@ -20,13 +20,13 @@ classifiers = Operating System :: POSIX :: Linux License :: OSI Approved :: MIT License Intended Audience :: Developers - Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Topic :: Software Development Topic :: Software Development :: Libraries Topic :: System :: Hardware [options] +python_requires = >= 3.6 packages = enviroplus install_requires = pimoroni-bme280 @@ -54,14 +54,6 @@ ignore = [pimoroni] py2deps = - python-pip - python-numpy - python-smbus - python-pil - python-cffi - python-spidev - python-rpi.gpio - libportaudio2 py3deps = python3-pip python3-numpy diff --git a/library/tests/conftest.py b/library/tests/conftest.py index 8a5c54c..b3fa376 100644 --- a/library/tests/conftest.py +++ b/library/tests/conftest.py @@ -1,90 +1,115 @@ -"""Test configuration. -These allow the mocking of various Python modules -that might otherwise have runtime side-effects. -""" -import sys -import mock -import pytest -from i2cdevice import MockSMBus - - -class SMBusFakeDevice(MockSMBus): - def __init__(self, i2c_bus): - MockSMBus.__init__(self, i2c_bus) - self.regs[0x00:0x01] = 0x0f, 0x00 - - -@pytest.fixture(scope='function', autouse=True) -def cleanup(): - yield None - try: - del sys.modules['enviroplus'] - except KeyError: - pass - try: - del sys.modules['enviroplus.noise'] - except KeyError: - pass - try: - del sys.modules['enviroplus.gas'] - except KeyError: - pass - - -@pytest.fixture(scope='function', autouse=False) -def GPIO(): - """Mock RPi.GPIO module.""" - GPIO = mock.MagicMock() - # Fudge for Python < 37 (possibly earlier) - sys.modules['RPi'] = mock.Mock() - sys.modules['RPi'].GPIO = GPIO - sys.modules['RPi.GPIO'] = GPIO - yield GPIO - del sys.modules['RPi'] - del sys.modules['RPi.GPIO'] - - -@pytest.fixture(scope='function', autouse=False) -def spidev(): - """Mock spidev module.""" - spidev = mock.MagicMock() - sys.modules['spidev'] = spidev - yield spidev - del sys.modules['spidev'] - - -@pytest.fixture(scope='function', autouse=False) -def smbus(): - """Mock smbus module.""" - smbus = mock.MagicMock() - smbus.SMBus = SMBusFakeDevice - sys.modules['smbus'] = smbus - yield smbus - del sys.modules['smbus'] - - -@pytest.fixture(scope='function', autouse=False) -def atexit(): - """Mock atexit module.""" - atexit = mock.MagicMock() - sys.modules['atexit'] = atexit - yield atexit - del sys.modules['atexit'] - - -@pytest.fixture(scope='function', autouse=False) -def sounddevice(): - """Mock sounddevice module.""" - sounddevice = mock.MagicMock() - sys.modules['sounddevice'] = sounddevice - yield sounddevice - del sys.modules['sounddevice'] - - -@pytest.fixture(scope='function', autouse=False) -def numpy(): - """Mock numpy module.""" - numpy = mock.MagicMock() - sys.modules['numpy'] = numpy - yield numpy - del sys.modules['numpy'] +"""Test configuration. +These allow the mocking of various Python modules +that might otherwise have runtime side-effects. +""" +import sys +import mock +import pytest +from i2cdevice import MockSMBus + + +class SMBusFakeDevice(MockSMBus): + def __init__(self, i2c_bus): + MockSMBus.__init__(self, i2c_bus) + self.regs[0x00:0x01] = 0x0f, 0x00 + + +class SMBusFakeDeviceNoTimeout(MockSMBus): + def __init__(self, i2c_bus): + MockSMBus.__init__(self, i2c_bus) + self.regs[0x00:0x01] = 0x0f, 0x80 + + +@pytest.fixture(scope='function', autouse=True) +def cleanup(): + yield None + try: + del sys.modules['enviroplus'] + except KeyError: + pass + try: + del sys.modules['enviroplus.noise'] + except KeyError: + pass + try: + del sys.modules['enviroplus.gas'] + except KeyError: + pass + + +@pytest.fixture(scope='function', autouse=False) +def GPIO(): + """Mock RPi.GPIO module.""" + GPIO = mock.MagicMock() + # Fudge for Python < 37 (possibly earlier) + sys.modules['RPi'] = mock.Mock() + sys.modules['RPi'].GPIO = GPIO + sys.modules['RPi.GPIO'] = GPIO + yield GPIO + del sys.modules['RPi'] + del sys.modules['RPi.GPIO'] + + +@pytest.fixture(scope='function', autouse=False) +def spidev(): + """Mock spidev module.""" + spidev = mock.MagicMock() + sys.modules['spidev'] = spidev + yield spidev + del sys.modules['spidev'] + + +@pytest.fixture(scope='function', autouse=False) +def smbus(): + """Mock smbus module.""" + smbus = mock.MagicMock() + smbus.SMBus = SMBusFakeDevice + sys.modules['smbus'] = smbus + yield smbus + del sys.modules['smbus'] + + +@pytest.fixture(scope='function', autouse=False) +def smbus_notimeout(): + """Mock smbus module.""" + smbus = mock.MagicMock() + smbus.SMBus = SMBusFakeDeviceNoTimeout + sys.modules['smbus'] = smbus + yield smbus + del sys.modules['smbus'] + + +@pytest.fixture(scope='function', autouse=False) +def mocksmbus(): + """Mock smbus module.""" + smbus = mock.MagicMock() + sys.modules['smbus'] = smbus + yield smbus + del sys.modules['smbus'] + + +@pytest.fixture(scope='function', autouse=False) +def atexit(): + """Mock atexit module.""" + atexit = mock.MagicMock() + sys.modules['atexit'] = atexit + yield atexit + del sys.modules['atexit'] + + +@pytest.fixture(scope='function', autouse=False) +def sounddevice(): + """Mock sounddevice module.""" + sounddevice = mock.MagicMock() + sys.modules['sounddevice'] = sounddevice + yield sounddevice + del sys.modules['sounddevice'] + + +@pytest.fixture(scope='function', autouse=False) +def numpy(): + """Mock numpy module.""" + numpy = mock.MagicMock() + sys.modules['numpy'] = numpy + yield numpy + del sys.modules['numpy'] diff --git a/library/tests/test_noise.py b/library/tests/test_noise.py index 3778c16..75aa89c 100644 --- a/library/tests/test_noise.py +++ b/library/tests/test_noise.py @@ -1,48 +1,48 @@ -import pytest - - -def test_noise_setup(sounddevice, numpy): - from enviroplus.noise import Noise - - noise = Noise(sample_rate=16000, duration=0.1) - del noise - - -def test_noise_get_amplitudes_at_frequency_ranges(sounddevice, numpy): - from enviroplus.noise import Noise - - noise = Noise(sample_rate=16000, duration=0.1) - noise.get_amplitudes_at_frequency_ranges([ - (100, 500), - (501, 1000) - ]) - - sounddevice.rec.assert_called_with(0.1 * 16000, samplerate=16000, blocking=True, channels=1, dtype='float64') - - -def test_noise_get_noise_profile(sounddevice, numpy): - from enviroplus.noise import Noise - - numpy.mean.return_value = 10.0 - - noise = Noise(sample_rate=16000, duration=0.1) - amp_low, amp_mid, amp_high, amp_total = noise.get_noise_profile( - noise_floor=100, - low=0.12, - mid=0.36, - high=None) - - sounddevice.rec.assert_called_with(0.1 * 16000, samplerate=16000, blocking=True, channels=1, dtype='float64') - - assert amp_total == 10.0 - - -def test_get_amplitude_at_frequency_range(sounddevice, numpy): - from enviroplus.noise import Noise - - noise = Noise(sample_rate=16000, duration=0.1) - - noise.get_amplitude_at_frequency_range(0, 8000) - - with pytest.raises(ValueError): - noise.get_amplitude_at_frequency_range(0, 16000) +import pytest + + +def test_noise_setup(sounddevice, numpy): + from enviroplus.noise import Noise + + noise = Noise(sample_rate=16000, duration=0.1) + del noise + + +def test_noise_get_amplitudes_at_frequency_ranges(sounddevice, numpy): + from enviroplus.noise import Noise + + noise = Noise(sample_rate=16000, duration=0.1) + noise.get_amplitudes_at_frequency_ranges([ + (100, 500), + (501, 1000) + ]) + + sounddevice.rec.assert_called_with(0.1 * 16000, samplerate=16000, blocking=True, channels=1, dtype='float64') + + +def test_noise_get_noise_profile(sounddevice, numpy): + from enviroplus.noise import Noise + + numpy.mean.return_value = 10.0 + + noise = Noise(sample_rate=16000, duration=0.1) + amp_low, amp_mid, amp_high, amp_total = noise.get_noise_profile( + noise_floor=100, + low=0.12, + mid=0.36, + high=None) + + sounddevice.rec.assert_called_with(0.1 * 16000, samplerate=16000, blocking=True, channels=1, dtype='float64') + + assert amp_total == 10.0 + + +def test_get_amplitude_at_frequency_range(sounddevice, numpy): + from enviroplus.noise import Noise + + noise = Noise(sample_rate=16000, duration=0.1) + + noise.get_amplitude_at_frequency_range(0, 8000) + + with pytest.raises(ValueError): + noise.get_amplitude_at_frequency_range(0, 16000) diff --git a/library/tests/test_setup.py b/library/tests/test_setup.py index 2aa7b49..40bf80d 100644 --- a/library/tests/test_setup.py +++ b/library/tests/test_setup.py @@ -1,3 +1,6 @@ +import pytest + + def test_gas_setup(GPIO, smbus): from enviroplus import gas gas._is_setup = False @@ -5,6 +8,22 @@ def test_gas_setup(GPIO, smbus): gas.setup() +def test_gas_unavailable(GPIO, mocksmbus): + from enviroplus import gas + mocksmbus.SMBus(1).read_i2c_block_data.side_effect = IOError("Oh noes!") + gas._is_setup = False + assert gas.available() == False + + with pytest.raises(RuntimeError): + gas.read_all() + + +def test_gas_available(GPIO, smbus_notimeout): + from enviroplus import gas + gas._is_setup = False + assert gas.available() == True + + def test_gas_read_all(GPIO, smbus): from enviroplus import gas gas._is_setup = False diff --git a/library/tox.ini b/library/tox.ini index aa96216..fcee079 100644 --- a/library/tox.ini +++ b/library/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{27,35},qa +envlist = py{36, 37, 38, 39},qa skip_missing_interpreters = True [testenv]