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:
parent
9778000e9a
commit
edf20f542a
4 changed files with 129 additions and 10 deletions
|
@ -15,7 +15,7 @@ from homeassistant.components.device_tracker import (
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||||
from homeassistant.util import Throttle
|
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.
|
# Return cached results if last scan was less then this time ago.
|
||||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||||
|
|
|
@ -9,13 +9,19 @@ import socket
|
||||||
import threading
|
import threading
|
||||||
import datetime
|
import datetime
|
||||||
import time
|
import time
|
||||||
|
import re
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
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
|
from homeassistant.helpers.entity import Entity
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
|
REQUIREMENTS = ['fritzconnection==0.6.3']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
DEFAULT_NAME = 'Phone'
|
DEFAULT_NAME = 'Phone'
|
||||||
|
@ -27,13 +33,24 @@ VALUE_RING = 'ringing'
|
||||||
VALUE_CALL = 'dialing'
|
VALUE_CALL = 'dialing'
|
||||||
VALUE_CONNECT = 'talking'
|
VALUE_CONNECT = 'talking'
|
||||||
VALUE_DISCONNECT = 'idle'
|
VALUE_DISCONNECT = 'idle'
|
||||||
|
CONF_PHONEBOOK = 'phonebook'
|
||||||
|
CONF_PREFIXES = 'prefixes'
|
||||||
|
|
||||||
INTERVAL_RECONNECT = 60
|
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({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
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)
|
name = config.get(CONF_NAME)
|
||||||
host = config.get(CONF_HOST)
|
host = config.get(CONF_HOST)
|
||||||
port = config.get(CONF_PORT)
|
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])
|
add_devices([sensor])
|
||||||
|
|
||||||
monitor = FritzBoxCallMonitor(host=host, port=port, sensor=sensor)
|
monitor = FritzBoxCallMonitor(host=host, port=port, sensor=sensor)
|
||||||
monitor.connect()
|
monitor.connect()
|
||||||
|
|
||||||
|
def _stop_listener(_event):
|
||||||
|
monitor.stopped.set()
|
||||||
|
|
||||||
|
hass.bus.listen_once(
|
||||||
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
|
_stop_listener
|
||||||
|
)
|
||||||
|
|
||||||
if monitor.sock is None:
|
if monitor.sock is None:
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
|
@ -59,11 +99,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
class FritzBoxCallSensor(Entity):
|
class FritzBoxCallSensor(Entity):
|
||||||
"""Implementation of a Fritz!Box call monitor."""
|
"""Implementation of a Fritz!Box call monitor."""
|
||||||
|
|
||||||
def __init__(self, name):
|
def __init__(self, name, phonebook):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
self._state = VALUE_DEFAULT
|
self._state = VALUE_DEFAULT
|
||||||
self._attributes = {}
|
self._attributes = {}
|
||||||
self._name = name
|
self._name = name
|
||||||
|
self.phonebook = phonebook
|
||||||
|
|
||||||
def set_state(self, state):
|
def set_state(self, state):
|
||||||
"""Set the state."""
|
"""Set the state."""
|
||||||
|
@ -75,8 +116,11 @@ class FritzBoxCallSensor(Entity):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
"""No polling needed."""
|
"""Polling needed only to update phonebook, if defined."""
|
||||||
return False
|
if self.phonebook is None:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
|
@ -93,6 +137,18 @@ class FritzBoxCallSensor(Entity):
|
||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
return self._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):
|
class FritzBoxCallMonitor(object):
|
||||||
"""Event listener to monitor calls on the Fritz!Box."""
|
"""Event listener to monitor calls on the Fritz!Box."""
|
||||||
|
@ -103,6 +159,7 @@ class FritzBoxCallMonitor(object):
|
||||||
self.port = port
|
self.port = port
|
||||||
self.sock = None
|
self.sock = None
|
||||||
self._sensor = sensor
|
self._sensor = sensor
|
||||||
|
self.stopped = threading.Event()
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
"""Connect to the Fritz!Box."""
|
"""Connect to the Fritz!Box."""
|
||||||
|
@ -110,7 +167,7 @@ class FritzBoxCallMonitor(object):
|
||||||
self.sock.settimeout(10)
|
self.sock.settimeout(10)
|
||||||
try:
|
try:
|
||||||
self.sock.connect((self.host, self.port))
|
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:
|
except socket.error as err:
|
||||||
self.sock = None
|
self.sock = None
|
||||||
_LOGGER.error("Cannot connect to %s on port %s: %s",
|
_LOGGER.error("Cannot connect to %s on port %s: %s",
|
||||||
|
@ -118,7 +175,7 @@ class FritzBoxCallMonitor(object):
|
||||||
|
|
||||||
def _listen(self):
|
def _listen(self):
|
||||||
"""Listen to incoming or outgoing calls."""
|
"""Listen to incoming or outgoing calls."""
|
||||||
while True:
|
while not self.stopped.isSet():
|
||||||
try:
|
try:
|
||||||
response = self.sock.recv(2048)
|
response = self.sock.recv(2048)
|
||||||
except socket.timeout:
|
except socket.timeout:
|
||||||
|
@ -152,6 +209,7 @@ class FritzBoxCallMonitor(object):
|
||||||
"to": line[4],
|
"to": line[4],
|
||||||
"device": line[5],
|
"device": line[5],
|
||||||
"initiated": isotime}
|
"initiated": isotime}
|
||||||
|
att["from_name"] = self._sensor.number_to_name(att["from"])
|
||||||
self._sensor.set_attributes(att)
|
self._sensor.set_attributes(att)
|
||||||
elif line[1] == "CALL":
|
elif line[1] == "CALL":
|
||||||
self._sensor.set_state(VALUE_CALL)
|
self._sensor.set_state(VALUE_CALL)
|
||||||
|
@ -160,13 +218,73 @@ class FritzBoxCallMonitor(object):
|
||||||
"to": line[5],
|
"to": line[5],
|
||||||
"device": line[6],
|
"device": line[6],
|
||||||
"initiated": isotime}
|
"initiated": isotime}
|
||||||
|
att["to_name"] = self._sensor.number_to_name(att["to"])
|
||||||
self._sensor.set_attributes(att)
|
self._sensor.set_attributes(att)
|
||||||
elif line[1] == "CONNECT":
|
elif line[1] == "CONNECT":
|
||||||
self._sensor.set_state(VALUE_CONNECT)
|
self._sensor.set_state(VALUE_CONNECT)
|
||||||
att = {"with": line[4], "device": [3], "accepted": isotime}
|
att = {"with": line[4], "device": [3], "accepted": isotime}
|
||||||
|
att["with_name"] = self._sensor.number_to_name(att["with"])
|
||||||
self._sensor.set_attributes(att)
|
self._sensor.set_attributes(att)
|
||||||
elif line[1] == "DISCONNECT":
|
elif line[1] == "DISCONNECT":
|
||||||
self._sensor.set_state(VALUE_DISCONNECT)
|
self._sensor.set_state(VALUE_DISCONNECT)
|
||||||
att = {"duration": line[3], "closed": isotime}
|
att = {"duration": line[3], "closed": isotime}
|
||||||
self._sensor.set_attributes(att)
|
self._sensor.set_attributes(att)
|
||||||
self._sensor.schedule_update_ha_state()
|
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'
|
||||||
|
|
|
@ -17,7 +17,7 @@ from homeassistant.util import Throttle
|
||||||
|
|
||||||
from requests.exceptions import RequestException
|
from requests.exceptions import RequestException
|
||||||
|
|
||||||
REQUIREMENTS = ['fritzconnection==0.6']
|
REQUIREMENTS = ['fritzconnection==0.6.3']
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)
|
||||||
|
|
||||||
|
|
|
@ -167,8 +167,9 @@ flux_led==0.15
|
||||||
freesms==0.1.1
|
freesms==0.1.1
|
||||||
|
|
||||||
# homeassistant.components.device_tracker.fritz
|
# homeassistant.components.device_tracker.fritz
|
||||||
|
# homeassistant.components.sensor.fritzbox_callmonitor
|
||||||
# homeassistant.components.sensor.fritzbox_netmonitor
|
# homeassistant.components.sensor.fritzbox_netmonitor
|
||||||
# fritzconnection==0.6
|
# fritzconnection==0.6.3
|
||||||
|
|
||||||
# homeassistant.components.switch.fritzdect
|
# homeassistant.components.switch.fritzdect
|
||||||
fritzhome==1.0.2
|
fritzhome==1.0.2
|
||||||
|
|
Loading…
Add table
Reference in a new issue