""" homeassistant.remote ~~~~~~~~~~~~~~~~~~~~ A module containing drop in replacements for core parts that will interface with a remote instance of home assistant. If a connection error occurs while communicating with the API a HomeAssistantException will be raised. """ import threading import logging import json import urlparse import requests import homeassistant as ha import homeassistant.httpinterface as hah METHOD_GET = "get" METHOD_POST = "post" def _setup_call_api(host, port, api_password): """ Helper method to setup a call api method. """ port = port or hah.SERVER_PORT base_url = "http://{}:{}".format(host, port) def _call_api(method, path, data=None): """ Makes a call to the Home Assistant api. """ data = data or {} data['api_password'] = api_password url = urlparse.urljoin(base_url, path) if method == METHOD_GET: return requests.get(url, params=data) else: return requests.request(method, url, data=data) return _call_api class EventBus(ha.EventBus): """ Drop-in replacement for a normal eventbus that will forward events to a remote eventbus. """ def __init__(self, host, api_password, port=None): ha.EventBus.__init__(self) self._call_api = _setup_call_api(host, port, api_password) self.logger = logging.getLogger(__name__) @property def listeners(self): """ List of events that is being listened for. """ try: req = self._call_api(METHOD_GET, hah.URL_API_EVENTS) if req.status_code == 200: data = req.json() return data['listeners'] else: raise ha.HomeAssistantException( "Got unexpected result (3): {}.".format(req.text)) except requests.exceptions.ConnectionError: self.logger.exception("EventBus:Error connecting to server") raise ha.HomeAssistantException("Error connecting to server") except ValueError: # If req.json() can't parse the json self.logger.exception("EventBus:Got unexpected result") raise ha.HomeAssistantException( "Got unexpected result: {}".format(req.text)) except KeyError: # If not all expected keys are in the returned JSON self.logger.exception("EventBus:Got unexpected result (2)") raise ha.HomeAssistantException( "Got unexpected result (2): {}".format(req.text)) def fire(self, event_type, event_data=None): """ Fire an event. """ data = {'event_data': json.dumps(event_data)} if event_data else None try: req = self._call_api(METHOD_POST, hah.URL_API_EVENTS_EVENT.format(event_type), data) if req.status_code != 200: error = "Error firing event: {} - {}".format( req.status_code, req.text) self.logger.error("EventBus:{}".format(error)) raise ha.HomeAssistantException(error) except requests.exceptions.ConnectionError: self.logger.exception("EventBus:Error connecting to server") def listen(self, event_type, listener): """ Not implemented for remote eventbus. Will throw NotImplementedError. """ raise NotImplementedError def remove_listener(self, event_type, listener): """ Not implemented for remote eventbus. Will throw NotImplementedError. """ raise NotImplementedError class StateMachine(ha.StateMachine): """ Drop-in replacement for a normal statemachine that communicates with a remote statemachine. """ def __init__(self, host, api_password, port=None): ha.StateMachine.__init__(self, None) self._call_api = _setup_call_api(host, port, api_password) self.lock = threading.Lock() self.logger = logging.getLogger(__name__) @property def categories(self): """ List of categories which states are being tracked. """ try: req = self._call_api(METHOD_GET, hah.URL_API_STATES) return req.json()['categories'] except requests.exceptions.ConnectionError: self.logger.exception("StateMachine:Error connecting to server") return [] except ValueError: # If req.json() can't parse the json self.logger.exception("StateMachine:Got unexpected result") return [] except KeyError: # If 'categories' key not in parsed json self.logger.exception("StateMachine:Got unexpected result (2)") return [] def set_state(self, category, new_state, attributes=None): """ Set the state of a category, add category if it does not exist. Attributes is an optional dict to specify attributes of this state. """ attributes = attributes or {} self.lock.acquire() data = {'new_state': new_state, 'attributes': json.dumps(attributes)} try: req = self._call_api(METHOD_POST, hah.URL_API_STATES_CATEGORY.format(category), data) if req.status_code != 201: error = "Error changing state: {} - {}".format( req.status_code, req.text) self.logger.error("StateMachine:{}".format(error)) raise ha.HomeAssistantException(error) except requests.exceptions.ConnectionError: self.logger.exception("StateMachine:Error connecting to server") raise ha.HomeAssistantException("Error connecting to server") finally: self.lock.release() def get_state(self, category): """ Returns a dict (state,last_changed, attributes) describing the state of the specified category. """ try: req = self._call_api(METHOD_GET, hah.URL_API_STATES_CATEGORY.format(category)) if req.status_code == 200: data = req.json() return ha.create_state(data['state'], data['attributes'], ha.str_to_datetime( data['last_changed'])) elif req.status_code == 422: # Category does not exist return None else: raise ha.HomeAssistantException( "Got unexpected result (3): {}.".format(req.text)) except requests.exceptions.ConnectionError: self.logger.exception("StateMachine:Error connecting to server") raise ha.HomeAssistantException("Error connecting to server") except ValueError: # If req.json() can't parse the json self.logger.exception("StateMachine:Got unexpected result") raise ha.HomeAssistantException( "Got unexpected result: {}".format(req.text)) except KeyError: # If not all expected keys are in the returned JSON self.logger.exception("StateMachine:Got unexpected result (2)") raise ha.HomeAssistantException( "Got unexpected result (2): {}".format(req.text))