Phone book lookup support for Fritz!Box call monitor (#6474)

* Update fritzconnection dependency in fritz and fritzbox_netmonitor components

* Add phone book name lookup support to FritzBox call monitor

* Updated requirements_all.txt

* Requested changes to FritzBox call monitor

* Listen to HOME_ASSISTANT_STOP and close thread

* Safe CPU time
This commit is contained in:
David Straub 2017-03-17 14:40:12 +01:00 committed by Pascal Vizeli
parent 9778000e9a
commit edf20f542a
4 changed files with 129 additions and 10 deletions

View file

@ -15,7 +15,7 @@ from homeassistant.components.device_tracker import (
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle
REQUIREMENTS = ['fritzconnection==0.6']
REQUIREMENTS = ['fritzconnection==0.6.3']
# Return cached results if last scan was less then this time ago.
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)

View file

@ -9,13 +9,19 @@ import socket
import threading
import datetime
import time
import re
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (CONF_HOST, CONF_PORT, CONF_NAME)
from homeassistant.const import (CONF_HOST, CONF_PORT, CONF_NAME,
CONF_PASSWORD, CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
REQUIREMENTS = ['fritzconnection==0.6.3']
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Phone'
@ -27,13 +33,24 @@ VALUE_RING = 'ringing'
VALUE_CALL = 'dialing'
VALUE_CONNECT = 'talking'
VALUE_DISCONNECT = 'idle'
CONF_PHONEBOOK = 'phonebook'
CONF_PREFIXES = 'prefixes'
INTERVAL_RECONNECT = 60
# Return cached results if phonebook was downloaded less then this time ago.
MIN_TIME_PHONEBOOK_UPDATE = datetime.timedelta(hours=6)
SCAN_INTERVAL = datetime.timedelta(hours=3)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_PASSWORD, default='admin'): cv.string,
vol.Optional(CONF_USERNAME, default=''): cv.string,
vol.Optional(CONF_PHONEBOOK, default=0): cv.positive_int,
vol.Optional(CONF_PREFIXES, default=[]): vol.All(cv.ensure_list,
[cv.string])
})
@ -42,14 +59,37 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
phonebook_id = config.get('phonebook')
prefixes = config.get('prefixes')
sensor = FritzBoxCallSensor(name=name)
try:
phonebook = FritzBoxPhonebook(host=host, port=port,
username=username, password=password,
phonebook_id=phonebook_id,
prefixes=prefixes)
# pylint: disable=bare-except
except:
phonebook = None
_LOGGER.warning('Phonebook with ID %s not found on Fritz!Box',
phonebook_id)
sensor = FritzBoxCallSensor(name=name, phonebook=phonebook)
add_devices([sensor])
monitor = FritzBoxCallMonitor(host=host, port=port, sensor=sensor)
monitor.connect()
def _stop_listener(_event):
monitor.stopped.set()
hass.bus.listen_once(
EVENT_HOMEASSISTANT_STOP,
_stop_listener
)
if monitor.sock is None:
return False
else:
@ -59,11 +99,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class FritzBoxCallSensor(Entity):
"""Implementation of a Fritz!Box call monitor."""
def __init__(self, name):
def __init__(self, name, phonebook):
"""Initialize the sensor."""
self._state = VALUE_DEFAULT
self._attributes = {}
self._name = name
self.phonebook = phonebook
def set_state(self, state):
"""Set the state."""
@ -75,8 +116,11 @@ class FritzBoxCallSensor(Entity):
@property
def should_poll(self):
"""No polling needed."""
return False
"""Polling needed only to update phonebook, if defined."""
if self.phonebook is None:
return False
else:
return True
@property
def state(self):
@ -93,6 +137,18 @@ class FritzBoxCallSensor(Entity):
"""Return the state attributes."""
return self._attributes
def number_to_name(self, number):
"""Return a name for a given phone number."""
if self.phonebook is None:
return 'unknown'
else:
return self.phonebook.get_name(number)
def update(self):
"""Update the phonebook if it is defined."""
if self.phonebook is not None:
self.phonebook.update_phonebook()
class FritzBoxCallMonitor(object):
"""Event listener to monitor calls on the Fritz!Box."""
@ -103,6 +159,7 @@ class FritzBoxCallMonitor(object):
self.port = port
self.sock = None
self._sensor = sensor
self.stopped = threading.Event()
def connect(self):
"""Connect to the Fritz!Box."""
@ -110,7 +167,7 @@ class FritzBoxCallMonitor(object):
self.sock.settimeout(10)
try:
self.sock.connect((self.host, self.port))
threading.Thread(target=self._listen, daemon=True).start()
threading.Thread(target=self._listen).start()
except socket.error as err:
self.sock = None
_LOGGER.error("Cannot connect to %s on port %s: %s",
@ -118,7 +175,7 @@ class FritzBoxCallMonitor(object):
def _listen(self):
"""Listen to incoming or outgoing calls."""
while True:
while not self.stopped.isSet():
try:
response = self.sock.recv(2048)
except socket.timeout:
@ -152,6 +209,7 @@ class FritzBoxCallMonitor(object):
"to": line[4],
"device": line[5],
"initiated": isotime}
att["from_name"] = self._sensor.number_to_name(att["from"])
self._sensor.set_attributes(att)
elif line[1] == "CALL":
self._sensor.set_state(VALUE_CALL)
@ -160,13 +218,73 @@ class FritzBoxCallMonitor(object):
"to": line[5],
"device": line[6],
"initiated": isotime}
att["to_name"] = self._sensor.number_to_name(att["to"])
self._sensor.set_attributes(att)
elif line[1] == "CONNECT":
self._sensor.set_state(VALUE_CONNECT)
att = {"with": line[4], "device": [3], "accepted": isotime}
att["with_name"] = self._sensor.number_to_name(att["with"])
self._sensor.set_attributes(att)
elif line[1] == "DISCONNECT":
self._sensor.set_state(VALUE_DISCONNECT)
att = {"duration": line[3], "closed": isotime}
self._sensor.set_attributes(att)
self._sensor.schedule_update_ha_state()
class FritzBoxPhonebook(object):
"""This connects to a FritzBox router and downloads its phone book."""
def __init__(self, host, port, username, password,
phonebook_id=0, prefixes=None):
"""Initialize the class."""
self.host = host
self.username = username
self.password = password
self.port = port
self.phonebook_id = phonebook_id
self.phonebook_dict = None
self.number_dict = None
self.prefixes = prefixes or []
# pylint: disable=import-error
import fritzconnection as fc
# Establish a connection to the FRITZ!Box.
self.fph = fc.FritzPhonebook(address=self.host,
user=self.username,
password=self.password)
if self.phonebook_id not in self.fph.list_phonebooks:
raise ValueError("Phonebook with this ID not found.")
self.update_phonebook()
@Throttle(MIN_TIME_PHONEBOOK_UPDATE)
def update_phonebook(self):
"""Update the phone book dictionary."""
self.phonebook_dict = self.fph.get_all_names(self.phonebook_id)
self.number_dict = {re.sub(r'[^\d\+]', '', nr): name
for name, nrs in self.phonebook_dict.items()
for nr in nrs}
_LOGGER.info('Fritz!Box phone book successfully updated.')
def get_name(self, number):
"""Return a name for a given phone number."""
number = re.sub(r'[^\d\+]', '', str(number))
if self.number_dict is None:
return 'unknown'
try:
return self.number_dict[number]
except KeyError:
pass
if self.prefixes:
for prefix in self.prefixes:
try:
return self.number_dict[prefix + number]
except KeyError:
pass
try:
return self.number_dict[prefix + number.lstrip('0')]
except KeyError:
pass
return 'unknown'

View file

@ -17,7 +17,7 @@ from homeassistant.util import Throttle
from requests.exceptions import RequestException
REQUIREMENTS = ['fritzconnection==0.6']
REQUIREMENTS = ['fritzconnection==0.6.3']
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)

View file

@ -167,8 +167,9 @@ flux_led==0.15
freesms==0.1.1
# homeassistant.components.device_tracker.fritz
# homeassistant.components.sensor.fritzbox_callmonitor
# homeassistant.components.sensor.fritzbox_netmonitor
# fritzconnection==0.6
# fritzconnection==0.6.3
# homeassistant.components.switch.fritzdect
fritzhome==1.0.2