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)