8000 IMAP Unread sensor updated for async and push by amelchio · Pull Request #9562 · home-assistant/core · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

IMAP Unread sensor updated for async and push #9562

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Sep 26, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 115 additions & 40 deletions homeassistant/components/sensor/imap.py
10000
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,27 @@
https://home-assistant.io/components/sensor.imap/
"""
import logging
import asyncio
import async_timeout

import voluptuous as vol

from homeassistant.helpers.entity import Entity
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_NAME, CONF_PORT, CONF_USERNAME, CONF_PASSWORD)
CONF_NAME, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
EVENT_HOMEASSISTANT_STOP)
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv

_LOGGER = logging.getLogger(__name__)

CONF_SERVER = "server"
REQUIREMENTS = ['aioimaplib==0.7.12']

CONF_SERVER = 'server'

DEFAULT_PORT = 993

ICON = 'mdi:email-outline'

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
Expand All @@ -30,17 +37,20 @@
})


def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the IMAP platform."""
sensor = ImapSensor(
config.get(CONF_NAME, None), config.get(CONF_USERNAME),
config.get(CONF_PASSWORD), config.get(CONF_SERVER),
config.get(CONF_PORT))
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup 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))

if not (yield from sensor.connection()):
raise PlatformNotReady

if sensor.connection:
add_devices([sensor], True)
else:
return False
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, sensor.shutdown())
async_add_devices([sensor], True)


class ImapSensor(Entity):
Expand All @@ -54,45 +64,110 @@ def __init__(self, name, user, password, server, port):
self._server = server
self._port = port
self._unread_count = 0
self.connection = self._login()
self._connection = None
self._does_push = None
self._idle_loop_task = None

def _login(self):
"""Login and return an IMAP connection."""
import imaplib
try:
connection = imaplib.IMAP4_SSL(self._server, self._port)
connection.login(self._user, self._password)
return connection
except imaplib.IMAP4.error:
_LOGGER.error("Failed to login to %s.", self._server)
return False
@asyncio.coroutine
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 unread emails."""
return self._unread_count

def update(self):
"""Check the number of unread emails."""
import imaplib
try:
self.connection.select()
self._unread_count = len(self.connection.search(
None, 'UnSeen UnDeleted')[1][0].split())
except imaplib.IMAP4.error:
_LOGGER.info("Connection to %s lost, attempting to reconnect",
self._server)
try:
self.connection = self._login()
except imaplib.IMAP4.error:
_LOGGER.error("Failed to reconnect.")
@property
def available(self):
"""Return the availability of the device."""
return self._connection is not None

@property
def icon(self):
"""Return the icon to use in the frontend."""
return ICON
def should_poll(self):
"""Return if polling is needed."""
return not self._does_push

@asyncio.coroutine
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)
yield from self._connection.wait_hello_from_server()
yield from self._connection.login(self._user, self._password)
yield from self._connection.select()
self._does_push = self._connection.has_capability('IDLE')
except (aioimaplib.AioImapException, asyncio.TimeoutError):
self._connection = None

return self._connection

@asyncio.coroutine
def idle_loop(self):
"""Wait for data pushed from server."""
import aioimaplib

while True:
try:
if (yield from self.connection()):
yield from self.refresh_unread_count()
yield from self.async_update_ha_state()

idle = yield from self._connection.idle_start()
yield from self._connection.wait_server_push()
self._connection.idle_done()
with async_timeout.timeout(10):
yield from idle
else:
yield from self.async_update_ha_state()
except (aioimaplib.AioImapException, asyncio.TimeoutError):
self.disconnected()

@asyncio.coroutine
def async_update(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you pull data and have a push?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is because push support (the IDLE capability) is optional for an IMAP server so should_poll can be True or False depending on the server.

I have added a commit making this a bit more readable.

"""Periodic polling of state."""
import aioimaplib

try:
if (yield from self.connection()):
yield from self.refresh_unread_count()
except (aioimaplib.AioImapException, asyncio.TimeoutError):
self.disconnected()

@asyncio.coroutine
def refresh_unread_count(self):
"""Check the number of unread emails."""
if self._connection:
yield from self._connection.noop()
_, lines = yield from self._connection.search('UnSeen UnDeleted')
self._unread_count = len(lines[0].split())

def disconnected(self):
"""Forget the connection after it was lost."""
_LOGGER.warning("Lost %s (will attempt to reconnect)", self._server)
self._connection = None

@asyncio.coroutine
def shutdown(self):
"""Close resources."""
if self._connection:
if self._connection.has_pending_idle():
self._connection.idle_done()
yield from self._connection.logout()
if self._idle_loop_task:
self._idle_loop_task.cancel()
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ aiodns==1.1.1
# homeassistant.components.http
aiohttp_cors==0.5.3

# homeassistant.components.sensor.imap
aioimaplib==0.7.12

# homeassistant.components.light.lifx
aiolifx==0.6.0

Expand Down
0