"""IMAP sensor support.""" import asyncio import logging import async_timeout import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) CONF_SERVER = 'server' CONF_FOLDER = 'folder' CONF_SEARCH = 'search' DEFAULT_PORT = 993 ICON = 'mdi:email-outline' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_SERVER): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_FOLDER, default='INBOX'): cv.string, vol.Optional(CONF_SEARCH, default='UnSeen UnDeleted'): cv.string, }) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the IMAP platform.""" sensor = ImapSensor(config.get(CONF_NAME), config.get(CONF_USERNAME), config.get(CONF_PASSWORD), config.get(CONF_SERVER), config.get(CONF_PORT), config.get(CONF_FOLDER), config.get(CONF_SEARCH)) if not await sensor.connection(): raise PlatformNotReady hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, sensor.shutdown()) async_add_entities([sensor], True) class ImapSensor(Entity): """Representation of an IMAP sensor.""" def __init__(self, name, user, password, server, port, folder, search): """Initialize the sensor.""" self._name = name or user self._user = user self._password = password self._server = server self._port = port self._folder = folder self._email_count = None self._search = search self._connection = None self._does_push = None self._idle_loop_task = None async def async_added_to_hass(self): """Handle when an entity is about to be added to Home Assistant.""" if not self.should_poll: self._idle_loop_task = self.hass.loop.create_task(self.idle_loop()) @property def name(self): """Return the name of the sensor.""" return self._name @property def icon(self): """Return the icon to use in the frontend.""" return ICON @property def state(self): """Return the number of emails found.""" return self._email_count @property def available(self): """Return the availability of the device.""" return self._connection is not None @property def should_poll(self): """Return if polling is needed.""" return not self._does_push async def connection(self): """Return a connection to the server, establishing it if necessary.""" import aioimaplib if self._connection is None: try: self._connection = aioimaplib.IMAP4_SSL( self._server, self._port) await self._connection.wait_hello_from_server() await self._connection.login(self._user, self._password) await self._connection.select(self._folder) self._does_push = self._connection.has_capability('IDLE') except (aioimaplib.AioImapException, asyncio.TimeoutError): self._connection = None return self._connection async def idle_loop(self): """Wait for data pushed from server.""" import aioimaplib while True: try: if await self.connection(): await self.refresh_email_count() await self.async_update_ha_state() idle = await self._connection.idle_start() await self._connection.wait_server_push() self._connection.idle_done() with async_timeout.timeout(10): await idle else: await self.async_update_ha_state() except (aioimaplib.AioImapException, asyncio.TimeoutError): self.disconnected() async def async_update(self): """Periodic polling of state.""" import aioimaplib try: if await self.connection(): await self.refresh_email_count() except (aioimaplib.AioImapException, asyncio.TimeoutError): self.disconnected() async def refresh_email_count(self): """Check the number of found emails.""" if self._connection: await self._connection.noop() result, lines = await self._connection.search(self._search) if result == 'OK': self._email_count = len(lines[0].split()) else: _LOGGER.error("Can't parse IMAP server response to search " "'%s': %s / %s", self._search, result, lines[0]) def disconnected(self): """Forget the connection after it was lost.""" _LOGGER.warning("Lost %s (will attempt to reconnect)", self._server) self._connection = None async def shutdown(self): """Close resources.""" if self._connection: if self._connection.has_pending_idle(): self._connection.idle_done() await self._connection.logout() if self._idle_loop_task: self._idle_loop_task.cancel()