From 2cf78344af591190d958856754a833c5bb96af3e Mon Sep 17 00:00:00 2001 From: Lucien Van Elsen Date: Wed, 7 Jul 2021 03:29:00 -0400 Subject: [PATCH] Add a config setting to disable use of GPIO pins. This is useful if the Pi does not have the custom board installed, and you want to prevent it from interacting with pins that are in use for other purposes. Split tests into hermetic unit tests which do not interact with hardware, and integration tests which exercise real modem and indicator hardware. --- callattendant/app.cfg.example | 4 + callattendant/app.py | 6 +- callattendant/config.py | 3 + callattendant/hardware/indicators.py | 31 ++++--- callattendant/hardware/modem.py | 3 +- callattendant/messaging/voicemail.py | 6 +- integration_tests/__init__.py | 0 integration_tests/test_indicators.py | 94 ++++++++++++++++++++++ {tests => integration_tests}/test_modem.py | 0 tests/test_indicators.py | 22 ++--- tests/test_voicemail.py | 10 ++- 11 files changed, 143 insertions(+), 36 deletions(-) create mode 100644 integration_tests/__init__.py create mode 100644 integration_tests/test_indicators.py rename {tests => integration_tests}/test_modem.py (100%) diff --git a/callattendant/app.cfg.example b/callattendant/app.cfg.example index 14fe1b8..ed3ca9d 100644 --- a/callattendant/app.cfg.example +++ b/callattendant/app.cfg.example @@ -195,6 +195,10 @@ VOICE_MAIL_MENU_FILE = "resources/voice_mail_menu.wav" # This should not be changed/overridden except during development/testing #VOICE_MAIL_MESSAGE_FOLDER = "messages" +# Disable use of GPIO pins for displaying indicators. If custom board is not installed, this should be set to True +# to prevent conflicts with other uses of GPIO pins. +GPIO_DISABLED = False + # GPIO_LED_..._PIN: These values are the GPIO pin numbers attached to the LED indicators # GPIO_LED_..._BRIGHTNESS: These values are a percentage of brightness for the LED indicators when on. GPIO_LED_RING_PIN = 14 diff --git a/callattendant/app.py b/callattendant/app.py index 0470cd6..c740b76 100755 --- a/callattendant/app.py +++ b/callattendant/app.py @@ -69,10 +69,12 @@ def __init__(self, config): # Initialize the visual indicators (LEDs) self.approved_indicator = ApprovedIndicator( self.config.get("GPIO_LED_APPROVED_PIN"), - self.config.get("GPIO_LED_APPROVED_BRIGHTNESS", 100)) + self.config.get("GPIO_LED_APPROVED_BRIGHTNESS", 100), + self.config.get("GPIO_DISABLED", False)) self.blocked_indicator = BlockedIndicator( self.config.get("GPIO_LED_BLOCKED_PIN"), - self.config.get("GPIO_LED_BLOCKED_BRIGHTNESS", 100)) + self.config.get("GPIO_LED_BLOCKED_BRIGHTNESS", 100), + self.config.get("GPIO_DISABLED", False)) # Create (and open) the modem self.modem = Modem(self.config) self.config["MODEM_ONLINE"] = self.modem.is_open # signal the webapp not online diff --git a/callattendant/config.py b/callattendant/config.py index 90463f9..4b8efea 100644 --- a/callattendant/config.py +++ b/callattendant/config.py @@ -59,6 +59,7 @@ "VOICE_MAIL_MENU_FILE": "resources/voice_mail_menu.wav", "VOICE_MAIL_MESSAGE_FOLDER": "messages", + "GPIO_DISABLED": False, "GPIO_LED_RING_PIN": 14, "GPIO_LED_RING_BRIGHTNESS": 100, "GPIO_LED_APPROVED_PIN": 15, @@ -176,6 +177,8 @@ def validate(self): if not isinstance(self["BLOCK_ENABLED"], bool): print("* BLOCK_ENABLED should be a bool: {}".format(type(self["BLOCK_ENABLED"]))) success = False + if not isinstance(self["GPIO_DISABLED"], bool): + print("* GPIO_DISABLED should be a bool: {}".format(type(self["GPIO_DISABLED"]))) for mode in self["SCREENING_MODE"]: if mode not in ("whitelist", "blacklist"): diff --git a/callattendant/hardware/indicators.py b/callattendant/hardware/indicators.py index cc9c9de..1af3dfa 100644 --- a/callattendant/hardware/indicators.py +++ b/callattendant/hardware/indicators.py @@ -26,7 +26,8 @@ # See: https://gpiozero.readthedocs.io/en/stable/ # See: https://gpiozero.readthedocs.io/en/stable/api_output.html#led -from gpiozero import LED, PWMLED, LEDBoard, OutputDeviceError, LEDCollection +from gpiozero import Device, LED, PWMLED, LEDBoard, OutputDeviceError, LEDCollection +from gpiozero.pins.mock import MockFactory, MockPWMPin GPIO_RING = 14 GPIO_APPROVED = 15 @@ -223,7 +224,7 @@ class PWMLEDIndicator(object): A pulse-width modulated LED. """ - def __init__(self, gpio_pin, brightness=100): + def __init__(self, gpio_pin, brightness=100, gpio_disabled=False): """ Constructor of a PWM LED. :param gpio_pin: @@ -231,6 +232,10 @@ def __init__(self, gpio_pin, brightness=100): :param brightness: Brightness percentage. Defaults to 100%. """ + if gpio_disabled: + # Use 'fake' pins so rest of code can still be called, but with no hardware interaction + # https://gpiozero.readthedocs.io/en/stable/api_pins.html#mock-pins + Device.pin_factory = MockFactory(pin_class=MockPWMPin) self.led = PWMLED(gpio_pin) self.brightness = brightness / 100.0 # brightness value is from 0 to 1.0 @@ -258,8 +263,8 @@ class RingIndicator(PWMLEDIndicator): """ The ring indicator, activated when an incoming call is being received. """ - def __init__(self, gpio_pin=GPIO_RING, brightness=100): - super().__init__(gpio_pin, brightness) + def __init__(self, gpio_pin=GPIO_RING, brightness=100, gpio_disabled=False): + super().__init__(gpio_pin, brightness, gpio_disabled) def ring(self): self.blink() @@ -270,8 +275,8 @@ class ApprovedIndicator(PWMLEDIndicator): """ The approved indicator activated when a call from a permitted number is received. """ - def __init__(self, gpio_pin=GPIO_APPROVED, brightness=100): - super().__init__(gpio_pin, brightness) + def __init__(self, gpio_pin=GPIO_APPROVED, brightness=100, gpio_disabled=False): + super().__init__(gpio_pin, brightness, gpio_disabled) class BlockedIndicator(PWMLEDIndicator): @@ -279,8 +284,8 @@ class BlockedIndicator(PWMLEDIndicator): The blocked indicator activated when a call from a blocked number is received. """ - def __init__(self, gpio_pin=GPIO_BLOCKED, brightness=100): - super().__init__(gpio_pin, brightness) + def __init__(self, gpio_pin=GPIO_BLOCKED, brightness=100, gpio_disabled=False): + super().__init__(gpio_pin, brightness, gpio_disabled) class MessageIndicator(PWMLEDIndicator): @@ -288,8 +293,8 @@ class MessageIndicator(PWMLEDIndicator): The message indicator activated when the voice messaging features are used. """ - def __init__(self, gpio_pin=GPIO_MESSAGE, brightness=100): - super().__init__(gpio_pin, brightness) + def __init__(self, gpio_pin=GPIO_MESSAGE, brightness=100, gpio_disabled=False): + super().__init__(gpio_pin, brightness, gpio_disabled) def turn_off(self): print("{MSG LED OFF}") @@ -312,7 +317,11 @@ class MessageCountIndicator(object): """ The message count indicator displays the number of unplayed messages in the system. """ - def __init__(self, *pins, **kwargs): + def __init__(self, gpio_disabled, *pins, **kwargs): + if gpio_disabled: + # Use 'fake' pins so rest of code can still be called, but with no hardware interaction + # https://gpiozero.readthedocs.io/en/stable/api_pins.html#mock-pins + Device.pin_factory = MockFactory() if len(pins) > 0: self.seven_seg = SevenSegmentDisplay(*pins, **kwargs) else: diff --git a/callattendant/hardware/modem.py b/callattendant/hardware/modem.py index 0c855e3..ad90eba 100644 --- a/callattendant/hardware/modem.py +++ b/callattendant/hardware/modem.py @@ -149,7 +149,8 @@ def __init__(self, config): # Ring notifications self.ring_indicator = RingIndicator( self.config.get("GPIO_LED_RING_PIN"), - self.config.get("GPIO_LED_RING_BRIGHTNESS", 100)) + self.config.get("GPIO_LED_RING_BRIGHTNESS", 100), + self.config.get("GPIO_DISABLED", False)) self.ring_event = threading.Event() # Initialize the serial port attached to the physical modem diff --git a/callattendant/messaging/voicemail.py b/callattendant/messaging/voicemail.py index 94db500..c89c4b3 100644 --- a/callattendant/messaging/voicemail.py +++ b/callattendant/messaging/voicemail.py @@ -51,10 +51,12 @@ def __init__(self, db, config, modem): # Initialize the message indicators (LEDs) self.message_indicator = MessageIndicator( self.config.get("GPIO_LED_MESSAGE_PIN", GPIO_MESSAGE), - self.config.get("GPIO_LED_MESSAGE_BRIGHTNESS", 100)) + self.config.get("GPIO_LED_MESSAGE_BRIGHTNESS", 100), + self.config.get("GPIO_DISABLED", False) + ) pins = self.config.get("GPIO_LED_MESSAGE_COUNT_PINS", GPIO_MESSAGE_COUNT_PINS) kwargs = self.config.get("GPIO_LED_MESSAGE_COUNT_KWARGS", GPIO_MESSAGE_COUNT_KWARGS) - self.message_count_indicator = MessageCountIndicator(*pins, **kwargs) + self.message_count_indicator = MessageCountIndicator(self.config.get("GPIO_DISABLED", False), *pins, **kwargs) # Create the Message object used to interface with the DB self.messages = Message(db, config) diff --git a/integration_tests/__init__.py b/integration_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/integration_tests/test_indicators.py b/integration_tests/test_indicators.py new file mode 100644 index 0000000..86b67d1 --- /dev/null +++ b/integration_tests/test_indicators.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# test_indicators.py +# +# Copyright 2020 Bruce Schubert +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os +import time + +import pytest + +from config import Config +from callattendant.hardware.indicators import RingIndicator, ApprovedIndicator, BlockedIndicator, \ + MessageIndicator, MessageCountIndicator + +def test_multiple(): + + config = Config() + + ringer = RingIndicator(config["GPIO_LED_RING_PIN"], brightness=100, gpio_disabled=False) + approved = ApprovedIndicator(config["GPIO_LED_APPROVED_PIN"], brightness=25, gpio_disabled=False) + blocked = BlockedIndicator(config["GPIO_LED_BLOCKED_PIN"], brightness=25, gpio_disabled=False) + message = MessageIndicator(config["GPIO_LED_MESSAGE_PIN"], brightness=100, gpio_disabled=False) + + pins_tuple = config["GPIO_LED_MESSAGE_COUNT_PINS"] + kwargs_dict = config["GPIO_LED_MESSAGE_COUNT_KWARGS"] + message_count = MessageCountIndicator(False, *pins_tuple, **kwargs_dict) + + # ~ ringer = RingIndicator() + # ~ approved = ApprovedIndicator() + # ~ blocked = BlockedIndicator() + # ~ message = MessageIndicator() + # ~ message_count = MessageCountIndicator() + + for i in range(0, 16): + message_count.display_hex(i) + time.sleep(.5) + + print("[Visual Tests]") + + print("Turning on all LEDs for 5 seconds...") + ringer.turn_on() + approved.turn_on() + blocked.turn_on() + message.turn_on() + time.sleep(5) + + print("Blinking on all LEDs for 5 seconds...") + ringer.blink() + time.sleep(.1) + approved.blink() + time.sleep(.1) + blocked.blink() + time.sleep(.1) + message.blink() + time.sleep(5) + + print("Turning off all LEDs...") + ringer.turn_off() + approved.turn_off() + blocked.turn_off() + message.turn_off() + time.sleep(2) + + print("Test normal status") + ringer.ring() + message.pulse(), + message_count.display(2) + time.sleep(10) + + # Release GPIO pins + ringer.close() + approved.close() + blocked.close() + message.close() diff --git a/tests/test_modem.py b/integration_tests/test_modem.py similarity index 100% rename from tests/test_modem.py rename to integration_tests/test_modem.py diff --git a/tests/test_indicators.py b/tests/test_indicators.py index 6dc827a..379709e 100644 --- a/tests/test_indicators.py +++ b/tests/test_indicators.py @@ -32,22 +32,18 @@ from callattendant.hardware.indicators import RingIndicator, ApprovedIndicator, BlockedIndicator, \ MessageIndicator, MessageCountIndicator -# Skip the test when running under continous integraion -pytestmark = pytest.mark.skipif(os.getenv("CI") == "true", reason="Hardware not installed") - - def test_multiple(): config = Config() - ringer = RingIndicator(config["GPIO_LED_RING_PIN"], brightness=100) - approved = ApprovedIndicator(config["GPIO_LED_APPROVED_PIN"], brightness=25) - blocked = BlockedIndicator(config["GPIO_LED_BLOCKED_PIN"], brightness=25) - message = MessageIndicator(config["GPIO_LED_MESSAGE_PIN"], brightness=100) + ringer = RingIndicator(config["GPIO_LED_RING_PIN"], brightness=100, gpio_disabled=True) + approved = ApprovedIndicator(config["GPIO_LED_APPROVED_PIN"], brightness=25, gpio_disabled=True) + blocked = BlockedIndicator(config["GPIO_LED_BLOCKED_PIN"], brightness=25, gpio_disabled=True) + message = MessageIndicator(config["GPIO_LED_MESSAGE_PIN"], brightness=100, gpio_disabled=True) pins_tuple = config["GPIO_LED_MESSAGE_COUNT_PINS"] kwargs_dict = config["GPIO_LED_MESSAGE_COUNT_KWARGS"] - message_count = MessageCountIndicator(*pins_tuple, **kwargs_dict) + message_count = MessageCountIndicator(True, *pins_tuple, **kwargs_dict) # ~ ringer = RingIndicator() # ~ approved = ApprovedIndicator() @@ -57,7 +53,6 @@ def test_multiple(): for i in range(0, 16): message_count.display_hex(i) - time.sleep(.5) print("[Visual Tests]") @@ -66,30 +61,23 @@ def test_multiple(): approved.turn_on() blocked.turn_on() message.turn_on() - time.sleep(5) print("Blinking on all LEDs for 5 seconds...") ringer.blink() - time.sleep(.1) approved.blink() - time.sleep(.1) blocked.blink() - time.sleep(.1) message.blink() - time.sleep(5) print("Turning off all LEDs...") ringer.turn_off() approved.turn_off() blocked.turn_off() message.turn_off() - time.sleep(2) print("Test normal status") ringer.ring() message.pulse(), message_count.display(2) - time.sleep(10) # Release GPIO pins ringer.close() diff --git a/tests/test_voicemail.py b/tests/test_voicemail.py index b5cfae3..994873c 100644 --- a/tests/test_voicemail.py +++ b/tests/test_voicemail.py @@ -28,6 +28,7 @@ from tempfile import gettempdir import pytest +from unittest import mock from callattendant.config import Config from callattendant.hardware.modem import Modem @@ -54,6 +55,7 @@ def config(): config['DEBUG'] = True config['TESTING'] = True config['VOICE_MAIL_MESSAGE_FOLDER'] = gettempdir() + config['GPIO_DISABLED'] = True return config @@ -68,7 +70,11 @@ def logger(db, config): @pytest.fixture(scope='module') def modem(db, config): - modem = Modem(config) + def _fake_record_audio(filename, detect_silence=True): + _ = open(filename, 'w') + return True + modem = mock.create_autospec(Modem) + modem.record_audio = mock.Mock(side_effect=_fake_record_audio) yield modem modem.stop() @@ -81,8 +87,6 @@ def voicemail(db, config, modem): voicemail.stop() -# Skip the test when running under continous integraion -@pytest.mark.skipif(os.getenv("CI") == "true", reason="Hardware not installed") def test_multiple(voicemail, logger): call_no = logger.log_caller(caller)