Skip to content

Commit

Permalink
Add Initial Mailbox panel and sensor (home-assistant#8233)
Browse files Browse the repository at this point in the history
* Initial implementation of Asterisk Mailbox

* Rework asterisk_mbox handler to avoid using the hass.data hash.  Fix requirements.

* Handle potential asterisk server disconnect.  bump asterisk_mbox requirement to 0.4.0

* Use async method for mp3 fetch from server

* Add http as dependency

* Minor log fix. try to force Travis to rebuild

* Updates based on review

* Fix error handling as per review

* Fix error handling as per review

* Refactor voicemail into mailbox component

* Hide mailbox component from front page

* Add demo for mailbox

* Add tests for mailbox

* Remove asterisk_mbox sensor and replace with a generic mailbox sensor

* Fix linting errors

* Remove mailbox sensor.  Remove demo.mp3.  Split entity from platform object.

* Update mailbox test

* Update mailbox test

* Use events to indicate state change rather than entity last-updated

* Make mailbox platform calls async.  Fix other review concerns

* Rewrite mailbox tests to live at root level and be async.  Fixmailbox dependency on http

* Only store number of messages not content in mailbox entity
  • Loading branch information
PhracturedBlue authored and balloob committed Aug 6, 2017
1 parent 5696e38 commit d74f4ea
Show file tree
Hide file tree
Showing 9 changed files with 603 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ omit =
homeassistant/components/arlo.py
homeassistant/components/*/arlo.py

homeassistant/components/asterisk_mbox.py
homeassistant/components/*/asterisk_mbox.py

homeassistant/components/axis.py
homeassistant/components/*/axis.py

Expand Down
82 changes: 82 additions & 0 deletions homeassistant/components/asterisk_mbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Support for Asterisk Voicemail interface."""

import logging

import voluptuous as vol


import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.const import (CONF_HOST,
CONF_PORT, CONF_PASSWORD)

from homeassistant.core import callback
from homeassistant.helpers.dispatcher import (async_dispatcher_connect,
async_dispatcher_send)

REQUIREMENTS = ['asterisk_mbox==0.4.0']

SIGNAL_MESSAGE_UPDATE = 'asterisk_mbox.message_updated'
SIGNAL_MESSAGE_REQUEST = 'asterisk_mbox.message_request'

DOMAIN = 'asterisk_mbox'

_LOGGER = logging.getLogger(__name__)


CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PORT): int,
vol.Required(CONF_PASSWORD): cv.string,
}),
}, extra=vol.ALLOW_EXTRA)


def setup(hass, config):
"""Set up for the Asterisk Voicemail box."""
conf = config.get(DOMAIN)

host = conf.get(CONF_HOST)
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)

return True


class AsteriskData(object):
"""Store Asterisk mailbox data."""

def __init__(self, hass, host, port, password):
"""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 = []

async_dispatcher_connect(
self.hass, SIGNAL_MESSAGE_REQUEST, self._request_messages)

@callback
def handle_data(self, command, msg):
"""Handle changes to the mailbox."""
from asterisk_mbox.commands import CMD_MESSAGE_LIST

if command == CMD_MESSAGE_LIST:
_LOGGER.info("AsteriskVM sent updated message list")
self.messages = sorted(msg,
key=lambda item: item['info']['origtime'],
reverse=True)
async_dispatcher_send(self.hass, SIGNAL_MESSAGE_UPDATE,
self.messages)

@callback
def _request_messages(self):
"""Handle changes to the mailbox."""
_LOGGER.info("Requesting message list")
self.client.messages()
1 change: 1 addition & 0 deletions homeassistant/components/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
'sensor',
'switch',
'tts',
'mailbox',
]


Expand Down
254 changes: 254 additions & 0 deletions homeassistant/components/mailbox/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
"""
Provides functionality for mailboxes.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/mailbox/
"""

import asyncio
import logging
from contextlib import suppress
from datetime import timedelta

import async_timeout

from aiohttp import web
from aiohttp.web_exceptions import HTTPNotFound

from homeassistant.core import callback
from homeassistant.helpers import config_per_platform, discovery
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity
from homeassistant.components.http import HomeAssistantView
from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_prepare_setup_platform

DEPENDENCIES = ['http']
DOMAIN = 'mailbox'
EVENT = 'mailbox_updated'
CONTENT_TYPE_MPEG = 'audio/mpeg'
SCAN_INTERVAL = timedelta(seconds=30)
_LOGGER = logging.getLogger(__name__)


@asyncio.coroutine
def async_setup(hass, config):
"""Track states and offer events for mailboxes."""
mailboxes = []
hass.components.frontend.register_built_in_panel(
'mailbox', 'Mailbox', 'mdi:account-location')
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):
"""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(
hass, config, DOMAIN, p_type)

if platform is None:
_LOGGER.error("Unknown mailbox platform specified")
return

_LOGGER.info("Setting up %s.%s", DOMAIN, p_type)
mailbox = None
try:
if hasattr(platform, 'async_get_handler'):
mailbox = yield from \
platform.async_get_handler(hass, p_config, discovery_info)
elif hasattr(platform, 'get_handler'):
mailbox = yield from hass.async_add_job(
platform.get_handler, hass, p_config, discovery_info)
else:
raise HomeAssistantError("Invalid mailbox platform.")

if mailbox is None:
_LOGGER.error(
"Failed to initialize mailbox platform %s", p_type)
return

except Exception: # pylint: disable=broad-except
_LOGGER.exception('Error setting up platform %s', p_type)
return

mailboxes.append(mailbox)
mailbox_entity = MailboxEntity(hass, mailbox)
component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
yield from component.async_add_entity(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)

@asyncio.coroutine
def async_platform_discovered(platform, info):
"""Handle for discovered platform."""
yield from 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."""

def __init__(self, hass, mailbox):
"""Initialize mailbox entity."""
self.mailbox = mailbox
self.hass = hass
self.message_count = 0

@callback
def _mailbox_updated(event):
self.hass.async_add_job(self.async_update_ha_state(True))

hass.bus.async_listen(EVENT, _mailbox_updated)

@property
def state(self):
"""Return the state of the binary sensor."""
return str(self.message_count)

@property
def name(self):
"""Return the name of the entity."""
return self.mailbox.name

@asyncio.coroutine
def async_update(self):
"""Retrieve messages from platform."""
messages = yield from self.mailbox.async_get_messages()
self.message_count = len(messages)


class Mailbox(object):
"""Represent an mailbox device."""

def __init__(self, hass, name):
"""Initialize mailbox object."""
self.hass = hass
self.name = name

def async_update(self):
"""Send event notification of updated mailbox."""
self.hass.bus.async_fire(EVENT)

@property
def media_type(self):
"""Return the supported media type."""
raise NotImplementedError()

@asyncio.coroutine
def async_get_media(self, msgid):
"""Return the media blob for the msgid."""
raise NotImplementedError()

@asyncio.coroutine
def async_get_messages(self):
"""Return a list of the current messages."""
raise NotImplementedError()

def async_delete(self, msgid):
"""Delete the specified messages."""
raise NotImplementedError()


class StreamError(Exception):
"""Media streaming exception."""

pass


class MailboxView(HomeAssistantView):
"""Base mailbox view."""

def __init__(self, mailboxes):
"""Initialize a basic mailbox view."""
self.mailboxes = mailboxes

def get_mailbox(self, platform):
"""Retrieve the specified mailbox."""
for mailbox in self.mailboxes:
if mailbox.name == platform:
return mailbox
raise HTTPNotFound


class MailboxPlatformsView(MailboxView):
"""View to return the list of mailbox platforms."""

url = "/api/mailbox/platforms"
name = "api:mailbox:platforms"

@asyncio.coroutine
def get(self, request):
"""Retrieve list of platforms."""
platforms = []
for mailbox in self.mailboxes:
platforms.append(mailbox.name)
return self.json(platforms)


class MailboxMessageView(MailboxView):
"""View to return the list of messages."""

url = "/api/mailbox/messages/{platform}"
name = "api:mailbox:messages"

@asyncio.coroutine
def get(self, request, platform):
"""Retrieve messages."""
mailbox = self.get_mailbox(platform)
messages = yield from mailbox.async_get_messages()
return self.json(messages)


class MailboxDeleteView(MailboxView):
"""View to delete selected messages."""

url = "/api/mailbox/delete/{platform}/{msgid}"
name = "api:mailbox:delete"

@asyncio.coroutine
def delete(self, request, platform, msgid):
"""Delete items."""
mailbox = self.get_mailbox(platform)
mailbox.async_delete(msgid)


class MailboxMediaView(MailboxView):
"""View to return a media file."""

url = r"/api/mailbox/media/{platform}/{msgid}"
name = "api:asteriskmbox:media"

@asyncio.coroutine
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)
except StreamError as err:
error_msg = "Error getting media: %s" % (err)
_LOGGER.error(error_msg)
return web.Response(status=500)
if stream:
return web.Response(body=stream,
content_type=mailbox.media_type)

return web.Response(status=500)
Loading

0 comments on commit d74f4ea

Please sign in to comment.