Skip to content

Commit

Permalink
Merge pull request #135 from emxsys/actions
Browse files Browse the repository at this point in the history
Make "ignore call" and "answer call" actions explicit
  • Loading branch information
emxsys authored Nov 20, 2020
2 parents 852b907 + a8b075b commit 40355f1
Show file tree
Hide file tree
Showing 7 changed files with 328 additions and 146 deletions.
91 changes: 54 additions & 37 deletions callattendant/app.cfg.example
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,28 @@ SCREENING_MODE = ("whitelist", "blacklist")
# BLOCK_ENABLED: if True calls that fail screening will be blocked
BLOCK_ENABLED = True

# BLOCK_NAME_PATTERNS: A regex expression dict applied to the CID names

# BLOCK_NAME_PATTERNS: Block calls based on a RegEx expression dict applied
# to the CID names: {"regex": "description", ... }
# Example: {"V[0-9]{15}": "Telemarketer Caller ID", "O": "Unknown caller"}
BLOCK_NAME_PATTERNS = {"V[0-9]{15}": "Telemarketer Caller ID", }

# BLOCK_NUMBER_PATTERNS: A regx expression dict applied to the CID numbers
# BLOCK_NUMBER_PATTERNS: Block calls based on a dict of regular expressions
# applied to the CID numbers: {"regex": "description", ... }
# Example: {"P": "Private number", "O": "Out of area",}
BLOCK_NUMBER_PATTERNS = {}

# PERMIT_NAME_PATTERNS: Permit calls based on a RegEx expression dict
# applied to the CID names: {"regex": "description", ... }
# Example: {".*DOE": "Family", ".*SCHOOL": "Schools", }
PERMIT_NAME_PATTERNS = {}

# PERMIT_NUMBER_PATTERNS: Permit calls based on a regx expression dict
# applied to the CID numbers: {"regex": "description", ... }
# Example: {"01628": "My area", }
PERMIT_NUMBER_PATTERNS = {}


# BLOCK_SERVICE: The name of the online service used to lookup robocallers and spam numbers.
# Currently, only NOMOROBO is supported and it is for the USA. Areas outside the USA should set
# to blank. When the online service is blank (disabled), only the blacklist and blocked
Expand All @@ -83,47 +97,56 @@ BLOCK_NUMBER_PATTERNS = {}
BLOCK_SERVICE = "NOMOROBO"


# BLOCKED_ACTIONS: A tuple containing a combination of the following actions:
# "greeting", "record_message", "voice_mail".
# BLOCKED_ACTIONS: A tuple containing following actions:
# "ignore" -OR- a combination of the following:
# "answer", "greeting", "record_message", "voice_mail".
#
# These actions are performed before hanging up.
# Note: "ignore" and "answer" are mutually exclusive; one or the other is required.
# Note: "record_message" and "voice_mail" actions are mutually exclusive.
# Note: A trailing comma is REQUIRED for a tuple with just one item.
#
# Note: the "record_message", "voice_mail" actions are mutually exclusive.
# Also Note: A trailing comma is REQUIRED for a tuple with just one item
# Example: Take no action, just let the phone ring
# BLOCKED_ACTIONS = ("ignore",)
# NOTE: A tuple with one item requires a trailing comma; just like the example above
#
# Example: No actions, just hang_up
# BLOCKED_ACTIONS = ()
# Example: Just answer and hang_up
# BLOCKED_ACTIONS = ("answer",)
# NOTE: A tuple with one item requires a trailing comma; just like the example above
#
# Example: Play an announcement before hanging up
# BLOCKED_ACTIONS = ("greeting", )
# Example: Answer and play an announcement before hanging up
# BLOCKED_ACTIONS = ("answer", "greeting")
#
# Example: Record a message before hanging up, no keypress required
# BLOCKED_ACTIONS = ("record_message", )
# Example: Answer and record a message before hanging up;
# no keypress required
# BLOCKED_ACTIONS = ("answer", "record_message")
#
# Example: Option to record a message; keypress required to leave message
# BLOCKED_ACTIONS = ("voice_mail", )
# Example: Answer and go into the voice mail menu;
# a keypress is required to a leave message
# BLOCKED_ACTIONS = ("answer", "voice_mail")
#
# Example: Play announcment and record a message; no keypress required
# BLOCKED_ACTIONS = ("greeting", "record_message" )
# Example: Answer, play announcment and record a message;
# no keypress required
# BLOCKED_ACTIONS = ("answer", "greeting", "record_message")
#
# Example: Play announcment and voice mail menu; keypress required to leave message
# BLOCKED_ACTIONS = ("greeting", "voice_mail" )
# Example: Answer, play announcment and go into the voice mail menu;
# a keypress is required to leave message
# BLOCKED_ACTIONS = ("answer", "greeting", "voice_mail")
#
BLOCKED_ACTIONS = ("greeting", "voice_mail")

# BLOCKED_RINGS_BEFORE_ANSWER: The number of rings to wait before answering
# Example: 0 to act immediately, possibly before your local phone rings.
BLOCKED_RINGS_BEFORE_ANSWER = 0
BLOCKED_ACTIONS = ("answer", "greeting", "voice_mail")

# BLOCKED_GREETING_FILE: The wav file to be played to blocked callers.
# Example: "Your number has been blocked."
BLOCKED_GREETING_FILE = "resources/blocked_greeting.wav"

# BLOCKED_RINGS_BEFORE_ANSWER: The number of rings to wait before answering
# Example: 0 to act immediately, possibly before your local phone rings.
BLOCKED_RINGS_BEFORE_ANSWER = 0


# SCREENED_ACTIONS: A tuple containing a combination of the following actions:
# "greeting", "record_message", "voice_mail". See BLOCKED_ACTIONS for more info.
SCREENED_ACTIONS = ("greeting", "record_message")
# "ignore" OR a combo of "answer", "greeting", "record_message", "voice_mail".
# See BLOCKED_ACTIONS for more info.
SCREENED_ACTIONS = ("answer", "greeting", "record_message")

# SCREENED_GREETING_FILE: The wav file to be played to allowed callers.
# Example: "I'm sorry, I can't take your call."
Expand All @@ -133,25 +156,19 @@ SCREENED_GREETING_FILE = "resources/general_greeting.wav"
# Example: 0 to act immediately, possibly before your local phone rings.
SCREENED_RINGS_BEFORE_ANSWER = 0

# PERMIT_NAME_PATTERNS: A regex expression dict applied to the CID names
# Example: {".*DOE": "Family maybe", "O": "Unknown caller", }
PERMIT_NAME_PATTERNS = {}

# PERMIT_NUMBER_PATTERNS: A regx expression dict applied to the CID numbers
# Example: {"01628": "My area", }
PERMIT_NUMBER_PATTERNS = {}

# PERMITTED_ACTIONS: A tuple containing a combination of the following actions:
# "greeting", "record_message", "voice_mail". See BLOCKED_ACTIONS for more info.
PERMITTED_ACTIONS = ("greeting", "record_message")
# "ignore" OR a combo of "answer", "greeting", "record_message", "voice_mail".
# See BLOCKED_ACTIONS for more info.
PERMITTED_ACTIONS = ("ignore")

# PERMITTED_GREETING_FILE: The wav file to be played to allowed callers.
# Example: "I'm sorry, I can't take your call."
PERMITTED_GREETING_FILE = "resources/general_greeting.wav"

# PERMITTED_RINGS_BEFORE_ANSWER: The number of rings to wait before answering
# PERMITTED_RINGS_BEFORE_ANSWER: The number of rings to wait before answering a call.
# Example: 4 to allow the callee to pick up the phone before going to voice mail.
PERMITTED_RINGS_BEFORE_ANSWER = 4
PERMITTED_RINGS_BEFORE_ANSWER = 0


# VOICE_MAIL_GREETING_FILE: The wav file played after answering: a general greeting
Expand Down
72 changes: 44 additions & 28 deletions callattendant/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,35 +189,14 @@ def run(self):
greeting = blocked_greeting_file
rings_before_answer = blocked["rings_before_answer"]

# Wait for the callee to answer the phone, if configured to do so.
# In North America, the standard ring cadence is "2-4", or two seconds
# of ringing followed by four seconds of silence (33% Duty Cycle). ring_count = 1 # Already had at least 1 ring to get here
RING_WAIT_SECS = 10.0
ok_to_answer = True
ring_count = 1 # Already had at least 1 ring to get here
last_ring = datetime.now()
while ring_count < rings_before_answer:
if not self._caller_queue.empty():
print(" > > > Another call has come in")
# Skip this call and process the next one
ok_to_answer = False
break
elif self.modem.ring_event.wait(1.0):
ring_count += 1
last_ring = datetime.now()
print(" > > > Ring count: {}".format(ring_count))
elif (datetime.now() - last_ring).total_seconds() > RING_WAIT_SECS:
# wait timeout; assume ringing has stopped before the ring count
# was reached because either the callee answered or caller hung up.
ok_to_answer = False
print(" > > > Ringing stopped: Caller hung up or callee answered")
break
# Waits for the callee to answer the phone, if configured to do so.
ok_to_answer = self.wait_for_rings(rings_before_answer)

# Answer the call!
if ok_to_answer:
if ok_to_answer and "answer" in actions:
self.answer_call(actions, greeting, call_no, caller)
else:
self.bypass_call(caller)
self.ignore_call(caller)

print("Waiting for next call...")

Expand Down Expand Up @@ -286,14 +265,51 @@ def answer_call(self, actions, greeting, call_no, caller):
# Go "on-hook"
self.modem.hang_up()

def bypass_call(self, caller):
def ignore_call(self, caller):
"""
Bpypas (do not answer) the call.
Ignore (do not answer) the call.
:param caller:
The caller ID data
"""
pass

def wait_for_rings(self, rings_before_answer):
"""
Waits for the given number of rings to occur.
:param rings_before_answer:
the number of rings to wait for.
:return:
True if the ring count meets or exceeds the rings before answer;
False if the rings stop or if another call comes in.
"""
# In North America, the standard ring cadence is "2-4", or two seconds
# of ringing followed by four seconds of silence (33% Duty Cycle).
RING_CADENCE = 6.0 # secs
RING_WAIT_SECS = RING_CADENCE + (RING_CADENCE * 0.5)
ok_to_answer = True
ring_count = 1 # Already had at least 1 ring to get here
last_ring = datetime.now()
while ring_count < rings_before_answer:
if not self._caller_queue.empty():
# Skip this call and process the next one
print(" > > > Another call has come in")
ok_to_answer = False
break
# Wait for a ring
elif self.modem.ring_event.wait(1.0):
# Increment the ring count and time of last ring
ring_count += 1
last_ring = datetime.now()
print(" > > > Ring count: {}".format(ring_count))
# On wait timeout, test for ringing stopped
elif (datetime.now() - last_ring).total_seconds() > RING_WAIT_SECS:
# Assume ringing has stopped before the ring count
# was reached because either the callee answered or caller hung up.
print(" > > > Ringing stopped: Caller hung up or callee answered")
ok_to_answer = False
break
return ok_to_answer


def make_config(filename=None, datapath=None, create_folder=False):
"""
Expand Down Expand Up @@ -437,7 +453,7 @@ def main(argv):

# Ensure all specified files exist and that values are conformant
if not config.validate():
print("Configuration is invalid. Please check {}".format(config_file))
print("ERROR: Configuration is invalid. Please check {}".format(config_file))
return 1

# Create and start the application
Expand Down
74 changes: 53 additions & 21 deletions callattendant/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
# and screened callers through to the home phone.
#
default_config = {
"VERSION": '1.1.0',
"VERSION": '1.1.1',

"ENV": 'production',
"DEBUG": False,
Expand All @@ -33,23 +33,24 @@

"BLOCK_ENABLED": True,
"BLOCK_SERVICE": "NOMOROBO",

"BLOCK_NAME_PATTERNS": {"V[0-9]{15}": "Telemarketer Caller ID", },
"BLOCK_NUMBER_PATTERNS": {},

"BLOCKED_ACTIONS": ("greeting", "record_message"),
"BLOCKED_RINGS_BEFORE_ANSWER": 0,
"PERMIT_NAME_PATTERNS": {},
"PERMIT_NUMBER_PATTERNS": {},

"BLOCKED_ACTIONS": ("answer", "greeting", "voice_mail"),
"BLOCKED_GREETING_FILE": "resources/blocked_greeting.wav",
"BLOCKED_RINGS_BEFORE_ANSWER": 0,

"SCREENED_ACTIONS": (),
"SCREENED_ACTIONS": ("answer", "greeting", "record_message"),
"SCREENED_GREETING_FILE": "resources/general_greeting.wav",
"SCREENED_RINGS_BEFORE_ANSWER": 0,

"PERMIT_NAME_PATTERNS": {},
"PERMIT_NUMBER_PATTERNS": {},

"PERMITTED_ACTIONS": (),
"PERMITTED_ACTIONS": ("ignore",),
"PERMITTED_GREETING_FILE": "resources/general_greeting.wav",
"PERMITTED_RINGS_BEFORE_ANSWER": 4,
"PERMITTED_RINGS_BEFORE_ANSWER": 0,

"VOICE_MAIL_GREETING_FILE": "resources/general_greeting.wav",
"VOICE_MAIL_GOODBYE_FILE": "resources/goodbye.wav",
Expand Down Expand Up @@ -181,18 +182,12 @@ def validate(self):
print("* SCREENING_MODE option is invalid: {}".format(mode))
success = False

for mode in self["BLOCKED_ACTIONS"]:
if mode not in ("greeting", "record_message", "voice_mail"):
print("* BLOCKED_ACTIONS option is invalid: {}".format(mode))
success = False
for mode in self["SCREENED_ACTIONS"]:
if mode not in ("greeting", "record_message", "voice_mail"):
print("* SCREENED_ACTIONS option is invalid: {}".format(mode))
success = False
for mode in self["PERMITTED_ACTIONS"]:
if mode not in ("greeting", "record_message", "voice_mail"):
print("* PERMITTED_ACTIONS option is invalid: {}".format(mode))
success = False
if not self._validate_actions("BLOCKED_ACTIONS"):
success = False
if not self._validate_actions("SCREENED_ACTIONS"):
success = False
if not self._validate_actions("PERMITTED_ACTIONS"):
success = False

if not isinstance(self["BLOCKED_RINGS_BEFORE_ANSWER"], int):
print("* BLOCKED_RINGS_BEFORE_ANSWER should be an integer: {}".format(type(self["BLOCKED_RINGS_BEFORE_ANSWER"])))
Expand Down Expand Up @@ -242,8 +237,45 @@ def validate(self):
print("* VOICE_MAIL_MESSAGE_FOLDER not found: {}".format(filepath))
success = False

# Warnings
if not self["PHONE_DISPLAY_SEPARATOR"] in self["PHONE_DISPLAY_FORMAT"]:
print("* WARNING: PHONE_DISPLAY_SEPARATOR not used in PHONE_DISPLAY_FORMAT: '{}'".format(self["PHONE_DISPLAY_SEPARATOR"]))

return success

def _validate_actions(self, key):
"""
:param key:
String: "BLOCKED_ACTIONS", "SCREENED_ACTIONS" or "PERMITTED_ACTIONS"
"""
if not isinstance(self[key], tuple):
print("* {} must be a tuple, not {}".format(key, type(self[key])))
return False

for action in self[key]:
if action not in ("answer", "ignore", "greeting", "record_message", "voice_mail"):
print("* {} option is invalid: {}".format(key, action))
return False

if not any(a in self[key] for a in ("answer", "ignore")):
print("* {} must include either 'answer' or 'ignore'".format(key))
return False

if all(a in self[key] for a in ("answer", "ignore")):
print("* {} cannot include both 'answer' and 'ignore'".format(key))
return False

if all(a in self[key] for a in ("record_message", "voice_mail")):
print("* {} cannot include both 'record_message' and 'voice_mail'".format(key))
return False

# WARNINGS: they print only; they do not fail validation.
if "ignore" in self[key]:
if any(a in self[key] for a in ("greeting", "record_message", "voice_mail")):
print("* WARNING: {} contains actions in addition to 'ignore'. They not be used.".format(key))

return True

def pretty_print(self):
"""
Pretty print the given configuration dict object.
Expand Down
2 changes: 1 addition & 1 deletion callattendant/hardware/modem.py
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,7 @@ def ring(self):
"""
Activate the ring indicator
"""
# Notify other threads that a ring occurred
# Notify other threads that a ring occurred
self.ring_event.set()
self.ring_event.clear()
# Visual notification (LED)
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

setuptools.setup(
name="callattendant", # Add user name when uploading to TestPyPI
version="1.1.0", # Ensure this is in-sync with VERSION in config.py
version="1.1.1", # Ensure this is in-sync with VERSION in config.py
author="Bruce Schubert",
author_email="[email protected]",
description="An automated call attendant and call blocker using a Raspberry Pi and USR-5637 modem",
Expand Down
Loading

0 comments on commit 40355f1

Please sign in to comment.