Add Call Data Log platform. Mailboxes no longer require media (#16579)

* Add multiple mailbox support

* Fix extraneous debugging

* Add cdr support

* liniting errors

* Mailbox log messages should mostly be debug.  Fix race condition with initializing CDR

* async decorators to async

* Lint fixes

* Typo

* remove unneeded parameter

* Fix variable names

* Fix async calls from worker thread.  Other minor cleanups

* more variable renames
This commit is contained in:
PhracturedBlue 2018-09-21 02:55:12 -07:00 committed by Paulus Schoutsen
parent df67093441
commit 98b92c78c0
7 changed files with 198 additions and 73 deletions

View file

@ -42,6 +42,7 @@ omit =
homeassistant/components/asterisk_mbox.py
homeassistant/components/*/asterisk_mbox.py
homeassistant/components/*/asterisk_cdr.py
homeassistant/components/august.py
homeassistant/components/*/august.py

View file

@ -13,7 +13,7 @@ from homeassistant.core import callback
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_send)
async_dispatcher_send, dispatcher_connect)
REQUIREMENTS = ['asterisk_mbox==0.5.0']
@ -21,8 +21,11 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = 'asterisk_mbox'
SIGNAL_DISCOVER_PLATFORM = "asterisk_mbox.discover_platform"
SIGNAL_MESSAGE_REQUEST = 'asterisk_mbox.message_request'
SIGNAL_MESSAGE_UPDATE = 'asterisk_mbox.message_updated'
SIGNAL_CDR_UPDATE = 'asterisk_mbox.message_updated'
SIGNAL_CDR_REQUEST = 'asterisk_mbox.message_request'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
@ -41,9 +44,7 @@ def setup(hass, config):
port = conf.get(CONF_PORT)
password = conf.get(CONF_PASSWORD)
hass.data[DOMAIN] = AsteriskData(hass, host, port, password)
discovery.load_platform(hass, 'mailbox', DOMAIN, {}, config)
hass.data[DOMAIN] = AsteriskData(hass, host, port, password, config)
return True
@ -51,31 +52,71 @@ def setup(hass, config):
class AsteriskData:
"""Store Asterisk mailbox data."""
def __init__(self, hass, host, port, password):
def __init__(self, hass, host, port, password, config):
"""Init the Asterisk data object."""
from asterisk_mbox import Client as asteriskClient
self.hass = hass
self.client = asteriskClient(host, port, password, self.handle_data)
self.messages = []
self.config = config
self.messages = None
self.cdr = None
async_dispatcher_connect(
dispatcher_connect(
self.hass, SIGNAL_MESSAGE_REQUEST, self._request_messages)
dispatcher_connect(
self.hass, SIGNAL_CDR_REQUEST, self._request_cdr)
dispatcher_connect(
self.hass, SIGNAL_DISCOVER_PLATFORM, self._discover_platform)
# Only connect after signal connection to ensure we don't miss any
self.client = asteriskClient(host, port, password, self.handle_data)
@callback
def _discover_platform(self, component):
_LOGGER.debug("Adding mailbox %s", component)
self.hass.async_create_task(discovery.async_load_platform(
self.hass, "mailbox", component, {}, self.config))
@callback
def handle_data(self, command, msg):
"""Handle changes to the mailbox."""
from asterisk_mbox.commands import CMD_MESSAGE_LIST
from asterisk_mbox.commands import (CMD_MESSAGE_LIST,
CMD_MESSAGE_CDR_AVAILABLE,
CMD_MESSAGE_CDR)
if command == CMD_MESSAGE_LIST:
_LOGGER.debug("AsteriskVM sent updated message list")
_LOGGER.debug("AsteriskVM sent updated message list: Len %d",
len(msg))
old_messages = self.messages
self.messages = sorted(
msg, key=lambda item: item['info']['origtime'], reverse=True)
async_dispatcher_send(
self.hass, SIGNAL_MESSAGE_UPDATE, self.messages)
if not isinstance(old_messages, list):
async_dispatcher_send(self.hass, SIGNAL_DISCOVER_PLATFORM,
DOMAIN)
async_dispatcher_send(self.hass, SIGNAL_MESSAGE_UPDATE,
self.messages)
elif command == CMD_MESSAGE_CDR:
_LOGGER.debug("AsteriskVM sent updated CDR list: Len %d",
len(msg.get('entries', [])))
self.cdr = msg['entries']
async_dispatcher_send(self.hass, SIGNAL_CDR_UPDATE, self.cdr)
elif command == CMD_MESSAGE_CDR_AVAILABLE:
if not isinstance(self.cdr, list):
_LOGGER.debug("AsteriskVM adding CDR platform")
self.cdr = []
async_dispatcher_send(self.hass, SIGNAL_DISCOVER_PLATFORM,
"asterisk_cdr")
async_dispatcher_send(self.hass, SIGNAL_CDR_REQUEST)
else:
_LOGGER.debug("AsteriskVM sent unknown message '%d' len: %d",
command, len(msg))
@callback
def _request_messages(self):
"""Handle changes to the mailbox."""
_LOGGER.debug("Requesting message list")
self.client.messages()
@callback
def _request_cdr(self):
"""Handle changes to the CDR."""
_LOGGER.debug("Requesting CDR list")
self.client.get_cdr()

View file

@ -23,36 +23,34 @@ from homeassistant.setup import async_prepare_setup_platform
_LOGGER = logging.getLogger(__name__)
CONTENT_TYPE_MPEG = 'audio/mpeg'
DEPENDENCIES = ['http']
DOMAIN = 'mailbox'
EVENT = 'mailbox_updated'
CONTENT_TYPE_MPEG = 'audio/mpeg'
CONTENT_TYPE_NONE = 'none'
SCAN_INTERVAL = timedelta(seconds=30)
@asyncio.coroutine
def async_setup(hass, config):
async def async_setup(hass, config):
"""Track states and offer events for mailboxes."""
mailboxes = []
yield from hass.components.frontend.async_register_built_in_panel(
await hass.components.frontend.async_register_built_in_panel(
'mailbox', 'mailbox', 'mdi:mailbox')
hass.http.register_view(MailboxPlatformsView(mailboxes))
hass.http.register_view(MailboxMessageView(mailboxes))
hass.http.register_view(MailboxMediaView(mailboxes))
hass.http.register_view(MailboxDeleteView(mailboxes))
@asyncio.coroutine
def async_setup_platform(p_type, p_config=None, discovery_info=None):
async def async_setup_platform(p_type, p_config=None, discovery_info=None):
"""Set up a mailbox platform."""
if p_config is None:
p_config = {}
if discovery_info is None:
discovery_info = {}
platform = yield from async_prepare_setup_platform(
platform = await async_prepare_setup_platform(
hass, config, DOMAIN, p_type)
if platform is None:
@ -63,10 +61,10 @@ def async_setup(hass, config):
mailbox = None
try:
if hasattr(platform, 'async_get_handler'):
mailbox = yield from \
mailbox = await \
platform.async_get_handler(hass, p_config, discovery_info)
elif hasattr(platform, 'get_handler'):
mailbox = yield from hass.async_add_job(
mailbox = await hass.async_add_executor_job(
platform.get_handler, hass, p_config, discovery_info)
else:
raise HomeAssistantError("Invalid mailbox platform.")
@ -81,21 +79,20 @@ def async_setup(hass, config):
return
mailboxes.append(mailbox)
mailbox_entity = MailboxEntity(hass, mailbox)
mailbox_entity = MailboxEntity(mailbox)
component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
yield from component.async_add_entities([mailbox_entity])
await component.async_add_entities([mailbox_entity])
setup_tasks = [async_setup_platform(p_type, p_config) for p_type, p_config
in config_per_platform(config, DOMAIN)]
if setup_tasks:
yield from asyncio.wait(setup_tasks, loop=hass.loop)
await asyncio.wait(setup_tasks, loop=hass.loop)
@asyncio.coroutine
def async_platform_discovered(platform, info):
async def async_platform_discovered(platform, info):
"""Handle for discovered platform."""
yield from async_setup_platform(platform, discovery_info=info)
await async_setup_platform(platform, discovery_info=info)
discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered)
@ -103,19 +100,21 @@ def async_setup(hass, config):
class MailboxEntity(Entity):
"""Entity for each mailbox platform."""
"""Entity for each mailbox platform to provide a badge display."""
def __init__(self, hass, mailbox):
def __init__(self, mailbox):
"""Initialize mailbox entity."""
self.mailbox = mailbox
self.hass = hass
self.message_count = 0
async def async_added_to_hass(self):
"""Complete entity initialization."""
@callback
def _mailbox_updated(event):
self.async_schedule_update_ha_state(True)
hass.bus.async_listen(EVENT, _mailbox_updated)
self.hass.bus.async_listen(EVENT, _mailbox_updated)
self.async_schedule_update_ha_state(True)
@property
def state(self):
@ -127,10 +126,9 @@ class MailboxEntity(Entity):
"""Return the name of the entity."""
return self.mailbox.name
@asyncio.coroutine
def async_update(self):
async def async_update(self):
"""Retrieve messages from platform."""
messages = yield from self.mailbox.async_get_messages()
messages = await self.mailbox.async_get_messages()
self.message_count = len(messages)
@ -151,13 +149,21 @@ class Mailbox:
"""Return the supported media type."""
raise NotImplementedError()
@asyncio.coroutine
def async_get_media(self, msgid):
@property
def can_delete(self):
"""Return if messages can be deleted."""
return False
@property
def has_media(self):
"""Return if messages have attached media files."""
return False
async def async_get_media(self, msgid):
"""Return the media blob for the msgid."""
raise NotImplementedError()
@asyncio.coroutine
def async_get_messages(self):
async def async_get_messages(self):
"""Return a list of the current messages."""
raise NotImplementedError()
@ -193,12 +199,16 @@ class MailboxPlatformsView(MailboxView):
url = "/api/mailbox/platforms"
name = "api:mailbox:platforms"
@asyncio.coroutine
def get(self, request):
async def get(self, request):
"""Retrieve list of platforms."""
platforms = []
for mailbox in self.mailboxes:
platforms.append(mailbox.name)
platforms.append(
{
'name': mailbox.name,
'has_media': mailbox.has_media,
'can_delete': mailbox.can_delete
})
return self.json(platforms)
@ -208,11 +218,10 @@ class MailboxMessageView(MailboxView):
url = "/api/mailbox/messages/{platform}"
name = "api:mailbox:messages"
@asyncio.coroutine
def get(self, request, platform):
async def get(self, request, platform):
"""Retrieve messages."""
mailbox = self.get_mailbox(platform)
messages = yield from mailbox.async_get_messages()
messages = await mailbox.async_get_messages()
return self.json(messages)
@ -222,8 +231,7 @@ class MailboxDeleteView(MailboxView):
url = "/api/mailbox/delete/{platform}/{msgid}"
name = "api:mailbox:delete"
@asyncio.coroutine
def delete(self, request, platform, msgid):
async def delete(self, request, platform, msgid):
"""Delete items."""
mailbox = self.get_mailbox(platform)
mailbox.async_delete(msgid)
@ -235,8 +243,7 @@ class MailboxMediaView(MailboxView):
url = r"/api/mailbox/media/{platform}/{msgid}"
name = "api:asteriskmbox:media"
@asyncio.coroutine
def get(self, request, platform, msgid):
async def get(self, request, platform, msgid):
"""Retrieve media."""
mailbox = self.get_mailbox(platform)
@ -244,7 +251,7 @@ class MailboxMediaView(MailboxView):
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
with async_timeout.timeout(10, loop=hass.loop):
try:
stream = yield from mailbox.async_get_media(msgid)
stream = await mailbox.async_get_media(msgid)
except StreamError as err:
error_msg = "Error getting media: %s" % (err)
_LOGGER.error(error_msg)

View file

@ -0,0 +1,64 @@
"""
Asterisk CDR interface.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/mailbox.asterisk_cdr/
"""
import logging
import hashlib
import datetime
from homeassistant.core import callback
from homeassistant.components.asterisk_mbox import SIGNAL_CDR_UPDATE
from homeassistant.components.asterisk_mbox import DOMAIN as ASTERISK_DOMAIN
from homeassistant.components.mailbox import Mailbox
from homeassistant.helpers.dispatcher import async_dispatcher_connect
DEPENDENCIES = ['asterisk_mbox']
_LOGGER = logging.getLogger(__name__)
MAILBOX_NAME = "asterisk_cdr"
async def async_get_handler(hass, config, discovery_info=None):
"""Set up the Asterix CDR platform."""
return AsteriskCDR(hass, MAILBOX_NAME)
class AsteriskCDR(Mailbox):
"""Asterisk VM Call Data Record mailbox."""
def __init__(self, hass, name):
"""Initialize Asterisk CDR."""
super().__init__(hass, name)
self.cdr = []
async_dispatcher_connect(
self.hass, SIGNAL_CDR_UPDATE, self._update_callback)
@callback
def _update_callback(self, msg):
"""Update the message count in HA, if needed."""
self._build_message()
self.async_update()
def _build_message(self):
"""Build message structure."""
cdr = []
for entry in self.hass.data[ASTERISK_DOMAIN].cdr:
timestamp = datetime.datetime.strptime(
entry['time'], "%Y-%m-%d %H:%M:%S").timestamp()
info = {
'origtime': timestamp,
'callerid': entry['callerid'],
'duration': entry['duration'],
}
sha = hashlib.sha256(str(entry).encode('utf-8')).hexdigest()
msg = "Destination: {}\nApplication: {}\n Context: {}".format(
entry['dest'], entry['application'], entry['context'])
cdr.append({'info': info, 'sha': sha, 'text': msg})
self.cdr = cdr
async def async_get_messages(self):
"""Return a list of the current messages."""
if not self.cdr:
self._build_message()
return self.cdr

View file

@ -4,10 +4,9 @@ Asterisk Voicemail interface.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/mailbox.asteriskvm/
"""
import asyncio
import logging
from homeassistant.components.asterisk_mbox import DOMAIN
from homeassistant.components.asterisk_mbox import DOMAIN as ASTERISK_DOMAIN
from homeassistant.components.mailbox import (
CONTENT_TYPE_MPEG, Mailbox, StreamError)
from homeassistant.core import callback
@ -21,10 +20,9 @@ SIGNAL_MESSAGE_REQUEST = 'asterisk_mbox.message_request'
SIGNAL_MESSAGE_UPDATE = 'asterisk_mbox.message_updated'
@asyncio.coroutine
def async_get_handler(hass, config, async_add_entities, discovery_info=None):
async def async_get_handler(hass, config, discovery_info=None):
"""Set up the Asterix VM platform."""
return AsteriskMailbox(hass, DOMAIN)
return AsteriskMailbox(hass, ASTERISK_DOMAIN)
class AsteriskMailbox(Mailbox):
@ -46,24 +44,32 @@ class AsteriskMailbox(Mailbox):
"""Return the supported media type."""
return CONTENT_TYPE_MPEG
@asyncio.coroutine
def async_get_media(self, msgid):
@property
def can_delete(self):
"""Return if messages can be deleted."""
return True
@property
def has_media(self):
"""Return if messages have attached media files."""
return True
async def async_get_media(self, msgid):
"""Return the media blob for the msgid."""
from asterisk_mbox import ServerError
client = self.hass.data[DOMAIN].client
client = self.hass.data[ASTERISK_DOMAIN].client
try:
return client.mp3(msgid, sync=True)
except ServerError as err:
raise StreamError(err)
@asyncio.coroutine
def async_get_messages(self):
async def async_get_messages(self):
"""Return a list of the current messages."""
return self.hass.data[DOMAIN].messages
return self.hass.data[ASTERISK_DOMAIN].messages
def async_delete(self, msgid):
"""Delete the specified messages."""
client = self.hass.data[DOMAIN].client
client = self.hass.data[ASTERISK_DOMAIN].client
_LOGGER.info("Deleting: %s", msgid)
client.delete(msgid)
return True

View file

@ -4,7 +4,6 @@ Asterisk Voicemail interface.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/mailbox.asteriskvm/
"""
import asyncio
from hashlib import sha1
import logging
import os
@ -15,13 +14,12 @@ from homeassistant.util import dt
_LOGGER = logging.getLogger(__name__)
DOMAIN = "DemoMailbox"
MAILBOX_NAME = "DemoMailbox"
@asyncio.coroutine
def async_get_handler(hass, config, discovery_info=None):
async def async_get_handler(hass, config, discovery_info=None):
"""Set up the Demo mailbox."""
return DemoMailbox(hass, DOMAIN)
return DemoMailbox(hass, MAILBOX_NAME)
class DemoMailbox(Mailbox):
@ -54,8 +52,17 @@ class DemoMailbox(Mailbox):
"""Return the supported media type."""
return CONTENT_TYPE_MPEG
@asyncio.coroutine
def async_get_media(self, msgid):
@property
def can_delete(self):
"""Return if messages can be deleted."""
return True
@property
def has_media(self):
"""Return if messages have attached media files."""
return True
async def async_get_media(self, msgid):
"""Return the media blob for the msgid."""
if msgid not in self._messages:
raise StreamError("Message not found")
@ -65,8 +72,7 @@ class DemoMailbox(Mailbox):
with open(audio_path, 'rb') as file:
return file.read()
@asyncio.coroutine
def async_get_messages(self):
async def async_get_messages(self):
"""Return a list of the current messages."""
return sorted(self._messages.values(),
key=lambda item: item['info']['origtime'],

View file

@ -29,7 +29,7 @@ def test_get_platforms_from_mailbox(mock_http_client):
req = yield from mock_http_client.get(url)
assert req.status == 200
result = yield from req.json()
assert len(result) == 1 and "DemoMailbox" in result
assert len(result) == 1 and "DemoMailbox" == result[0].get('name', None)
@asyncio.coroutine