forked from home-assistant/core
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Initial Mailbox panel and sensor (home-assistant#8233)
* 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
1 parent
5696e38
commit d74f4ea
Showing
9 changed files
with
603 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,6 +31,7 @@ | |
'sensor', | ||
'switch', | ||
'tts', | ||
'mailbox', | ||
] | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Oops, something went wrong.