diff --git a/.coveragerc b/.coveragerc index 2345cc13df2..39c2bce5ae5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -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 diff --git a/homeassistant/components/asterisk_mbox.py b/homeassistant/components/asterisk_mbox.py index 0d6d811db70..0907e48b256 100644 --- a/homeassistant/components/asterisk_mbox.py +++ b/homeassistant/components/asterisk_mbox.py @@ -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() diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index 0c5dabb6eeb..2ed12b23164 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -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) diff --git a/homeassistant/components/mailbox/asterisk_cdr.py b/homeassistant/components/mailbox/asterisk_cdr.py new file mode 100644 index 00000000000..ae0939c3da5 --- /dev/null +++ b/homeassistant/components/mailbox/asterisk_cdr.py @@ -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 diff --git a/homeassistant/components/mailbox/asterisk_mbox.py b/homeassistant/components/mailbox/asterisk_mbox.py index 29b34f3e512..087018084f2 100644 --- a/homeassistant/components/mailbox/asterisk_mbox.py +++ b/homeassistant/components/mailbox/asterisk_mbox.py @@ -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 diff --git a/homeassistant/components/mailbox/demo.py b/homeassistant/components/mailbox/demo.py index e0d2618ac4e..2aabde42b36 100644 --- a/homeassistant/components/mailbox/demo.py +++ b/homeassistant/components/mailbox/demo.py @@ -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'], diff --git a/tests/components/mailbox/test_init.py b/tests/components/mailbox/test_init.py index 3377fcefcf5..2c69a5effa7 100644 --- a/tests/components/mailbox/test_init.py +++ b/tests/components/mailbox/test_init.py @@ -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