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

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

Merged
merged 12 commits into from
Sep 21, 2018
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 54 additions & 13 deletions homeassistant/components/asterisk_mbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,19 @@
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']

_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({
Expand All @@ -41,41 +44,79 @@ 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


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()
83 changes: 45 additions & 38 deletions homeassistant/components/mailbox/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,36 +23,34 @@

_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:
Expand All @@ -63,10 +61,10 @@ def async_setup_platform(p_type, p_config=None, discovery_info=None):
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.")
Expand All @@ -81,41 +79,42 @@ def async_setup_platform(p_type, p_config=None, discovery_info=None):
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)

return True


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):
Expand All @@ -127,10 +126,9 @@ def name(self):
"""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)


Expand All @@ -151,13 +149,21 @@ def media_type(self):
"""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()

Expand Down Expand Up @@ -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)


Expand All @@ -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)


Expand All @@ -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)
Expand All @@ -235,16 +243,15 @@ 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)

hass = request.app['hass']
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)
Expand Down
64 changes: 64 additions & 0 deletions homeassistant/components/mailbox/asterisk_cdr.py
Original file line number Diff line number Diff line change
@@ -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
Loading