183 lines
5.7 KiB
Python
183 lines
5.7 KiB
Python
"""IMAP sensor support."""
|
|
import asyncio
|
|
import logging
|
|
|
|
from aioimaplib import IMAP4_SSL, AioImapException
|
|
import async_timeout
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
|
|
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
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
CONF_SERVER = "server"
|
|
CONF_FOLDER = "folder"
|
|
CONF_SEARCH = "search"
|
|
CONF_CHARSET = "charset"
|
|
|
|
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_CHARSET, default="utf-8"): cv.string,
|
|
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_CHARSET),
|
|
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(SensorEntity):
|
|
"""Representation of an IMAP sensor."""
|
|
|
|
def __init__(self, name, user, password, server, port, charset, folder, search):
|
|
"""Initialize the sensor."""
|
|
self._name = name or user
|
|
self._user = user
|
|
self._password = password
|
|
self._server = server
|
|
self._port = port
|
|
self._charset = charset
|
|
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."""
|
|
if self._connection is None:
|
|
try:
|
|
self._connection = 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 (AioImapException, asyncio.TimeoutError):
|
|
self._connection = None
|
|
|
|
return self._connection
|
|
|
|
async def idle_loop(self):
|
|
"""Wait for data pushed from server."""
|
|
while True:
|
|
try:
|
|
if await self.connection():
|
|
await self.refresh_email_count()
|
|
self.async_write_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:
|
|
self.async_write_ha_state()
|
|
except (AioImapException, asyncio.TimeoutError):
|
|
self.disconnected()
|
|
|
|
async def async_update(self):
|
|
"""Periodic polling of state."""
|
|
try:
|
|
if await self.connection():
|
|
await self.refresh_email_count()
|
|
except (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, charset=self._charset
|
|
)
|
|
|
|
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()
|