195 lines
6.0 KiB
Python
195 lines
6.0 KiB
Python
"""Module to handle messages from Home Assistant cloud."""
|
|
import asyncio
|
|
import logging
|
|
|
|
from aiohttp import hdrs, client_exceptions, WSMsgType
|
|
|
|
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
|
from homeassistant.components.alexa import smart_home
|
|
from homeassistant.util.decorator import Registry
|
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
from . import auth_api
|
|
|
|
|
|
HANDLERS = Registry()
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
class UnknownHandler(Exception):
|
|
"""Exception raised when trying to handle unknown handler."""
|
|
|
|
|
|
class CloudIoT:
|
|
"""Class to manage the IoT connection."""
|
|
|
|
def __init__(self, cloud):
|
|
"""Initialize the CloudIoT class."""
|
|
self.cloud = cloud
|
|
self.client = None
|
|
self.close_requested = False
|
|
self.tries = 0
|
|
|
|
@property
|
|
def is_connected(self):
|
|
"""Return if connected to the cloud."""
|
|
return self.client is not None
|
|
|
|
@asyncio.coroutine
|
|
def connect(self):
|
|
"""Connect to the IoT broker."""
|
|
if self.client is not None:
|
|
raise RuntimeError('Cannot connect while already connected')
|
|
|
|
self.close_requested = False
|
|
|
|
hass = self.cloud.hass
|
|
remove_hass_stop_listener = None
|
|
|
|
session = async_get_clientsession(self.cloud.hass)
|
|
|
|
@asyncio.coroutine
|
|
def _handle_hass_stop(event):
|
|
"""Handle Home Assistant shutting down."""
|
|
nonlocal remove_hass_stop_listener
|
|
remove_hass_stop_listener = None
|
|
yield from self.disconnect()
|
|
|
|
client = None
|
|
disconnect_warn = None
|
|
try:
|
|
yield from hass.async_add_job(auth_api.check_token, self.cloud)
|
|
|
|
self.client = client = yield from session.ws_connect(
|
|
self.cloud.relayer, headers={
|
|
hdrs.AUTHORIZATION:
|
|
'Bearer {}'.format(self.cloud.access_token)
|
|
})
|
|
self.tries = 0
|
|
|
|
remove_hass_stop_listener = hass.bus.async_listen_once(
|
|
EVENT_HOMEASSISTANT_STOP, _handle_hass_stop)
|
|
|
|
_LOGGER.info('Connected')
|
|
|
|
while not client.closed:
|
|
msg = yield from client.receive()
|
|
|
|
if msg.type in (WSMsgType.ERROR, WSMsgType.CLOSED,
|
|
WSMsgType.CLOSING):
|
|
disconnect_warn = 'Closed by server'
|
|
break
|
|
|
|
elif msg.type != WSMsgType.TEXT:
|
|
disconnect_warn = 'Received non-Text message: {}'.format(
|
|
msg.type)
|
|
break
|
|
|
|
try:
|
|
msg = msg.json()
|
|
except ValueError:
|
|
disconnect_warn = 'Received invalid JSON.'
|
|
break
|
|
|
|
_LOGGER.debug('Received message: %s', msg)
|
|
|
|
response = {
|
|
'msgid': msg['msgid'],
|
|
}
|
|
try:
|
|
result = yield from async_handle_message(
|
|
hass, self.cloud, msg['handler'], msg['payload'])
|
|
|
|
# No response from handler
|
|
if result is None:
|
|
continue
|
|
|
|
response['payload'] = result
|
|
|
|
except UnknownHandler:
|
|
response['error'] = 'unknown-handler'
|
|
|
|
except Exception: # pylint: disable=broad-except
|
|
_LOGGER.exception('Error handling message')
|
|
response['error'] = 'exception'
|
|
|
|
_LOGGER.debug('Publishing message: %s', response)
|
|
yield from client.send_json(response)
|
|
|
|
except auth_api.CloudError:
|
|
_LOGGER.warning('Unable to connect: Unable to refresh token.')
|
|
|
|
except client_exceptions.WSServerHandshakeError as err:
|
|
if err.code == 401:
|
|
disconnect_warn = 'Invalid auth.'
|
|
self.close_requested = True
|
|
# Should we notify user?
|
|
else:
|
|
_LOGGER.warning('Unable to connect: %s', err)
|
|
|
|
except client_exceptions.ClientError as err:
|
|
_LOGGER.warning('Unable to connect: %s', err)
|
|
|
|
except Exception: # pylint: disable=broad-except
|
|
if not self.close_requested:
|
|
_LOGGER.exception('Unexpected error')
|
|
|
|
finally:
|
|
if disconnect_warn is not None:
|
|
_LOGGER.warning('Connection closed: %s', disconnect_warn)
|
|
|
|
if remove_hass_stop_listener is not None:
|
|
remove_hass_stop_listener()
|
|
|
|
if client is not None:
|
|
self.client = None
|
|
yield from client.close()
|
|
|
|
if not self.close_requested:
|
|
self.tries += 1
|
|
|
|
# Sleep 0, 5, 10, 15 … up to 30 seconds between retries
|
|
yield from asyncio.sleep(
|
|
min(30, (self.tries - 1) * 5), loop=hass.loop)
|
|
|
|
hass.async_add_job(self.connect())
|
|
|
|
@asyncio.coroutine
|
|
def disconnect(self):
|
|
"""Disconnect the client."""
|
|
self.close_requested = True
|
|
yield from self.client.close()
|
|
|
|
|
|
@asyncio.coroutine
|
|
def async_handle_message(hass, cloud, handler_name, payload):
|
|
"""Handle incoming IoT message."""
|
|
handler = HANDLERS.get(handler_name)
|
|
|
|
if handler is None:
|
|
raise UnknownHandler()
|
|
|
|
return (yield from handler(hass, cloud, payload))
|
|
|
|
|
|
@HANDLERS.register('alexa')
|
|
@asyncio.coroutine
|
|
def async_handle_alexa(hass, cloud, payload):
|
|
"""Handle an incoming IoT message for Alexa."""
|
|
return (yield from smart_home.async_handle_message(hass, payload))
|
|
|
|
|
|
@HANDLERS.register('cloud')
|
|
@asyncio.coroutine
|
|
def async_handle_cloud(hass, cloud, payload):
|
|
"""Handle an incoming IoT message for cloud component."""
|
|
action = payload['action']
|
|
|
|
if action == 'logout':
|
|
yield from cloud.logout()
|
|
_LOGGER.error('You have been logged out from Home Assistant cloud: %s',
|
|
payload['reason'])
|
|
else:
|
|
_LOGGER.warning('Received unknown cloud action: %s', action)
|
|
|
|
return None
|