Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make "ignore call" and "answer call" actions explicit #135

Merged
merged 6 commits into from
Nov 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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