diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index b38eaddcd30..44793a938a6 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -26,7 +26,7 @@ TIMER_INTERVAL = 10 # seconds # every minute. assert 60 % TIMER_INTERVAL == 0, "60 % TIMER_INTERVAL should be 0!" -State = namedtuple("State", ['state', 'last_changed', 'attributes']) +DATE_STR_FORMAT = "%H:%M:%S %d-%m-%Y" def start_home_assistant(eventbus): """ Start home assistant. """ @@ -41,6 +41,14 @@ def start_home_assistant(eventbus): except KeyboardInterrupt: break +def datetime_to_str(dattim): + """ Converts datetime to a string format. """ + return dattim.strftime(DATE_STR_FORMAT) + +def str_to_datetime(dt_str): + """ Converts a string to a datetime object. """ + return datetime.strptime(dt_str, DATE_STR_FORMAT) + def ensure_list(parameter): """ Wraps parameter in a list if it is not one and returns it. """ return parameter if isinstance(parameter, list) else [parameter] @@ -52,6 +60,18 @@ def matcher(subject, pattern): """ return '*' in pattern or subject in pattern +def create_state(state, attributes=None, last_changed=None): + """ Creates a new state and initializes defaults where necessary. """ + attributes = attributes or {} + last_changed = last_changed or datetime.now() + + # We do not want microseconds, as they get lost when we do datetime_to_str + last_changed = last_changed.replace(microsecond=0) + + return {'state': state, + 'attributes': attributes, + 'last_changed': last_changed} + def track_state_change(eventbus, category, from_state, to_state, action): """ Helper method to track specific state changes. """ from_state = ensure_list(from_state) @@ -60,8 +80,8 @@ def track_state_change(eventbus, category, from_state, to_state, action): def listener(event): """ State change listener that listens for specific state changes. """ if category == event.data['category'] and \ - matcher(event.data['old_state'].state, from_state) and \ - matcher(event.data['new_state'].state, to_state): + matcher(event.data['old_state']['state'], from_state) and \ + matcher(event.data['new_state']['state'], to_state): action(event.data['category'], event.data['old_state'], @@ -81,21 +101,23 @@ def track_time_change(eventbus, action, def listener(event): """ Listens for matching time_changed events. """ - if (point_in_time and event.data['now'] > point_in_time) or \ + now = str_to_datetime(event.data['now']) + + if (point_in_time and now > point_in_time) or \ (not point_in_time and \ - matcher(event.data['now'].year, year) and \ - matcher(event.data['now'].month, month) and \ - matcher(event.data['now'].day, day) and \ - matcher(event.data['now'].hour, hour) and \ - matcher(event.data['now'].minute, minute) and \ - matcher(event.data['now'].second, second)): + matcher(now.year, year) and \ + matcher(now.month, month) and \ + matcher(now.day, day) and \ + matcher(now.hour, hour) and \ + matcher(now.minute, minute) and \ + matcher(now.second, second)): # point_in_time are exact points in time # so we always remove it after fire if listen_once or point_in_time: event.eventbus.remove_listener(EVENT_TIME_CHANGED, listener) - action(event.data['now']) + action(now) eventbus.listen(EVENT_TIME_CHANGED, listener) @@ -194,18 +216,16 @@ class StateMachine(object): # Add category if it does not exist if category not in self.states: - self.states[category] = State(new_state, datetime.now(), - attributes) + self.states[category] = create_state(new_state, attributes) # Change state and fire listeners else: old_state = self.states[category] - if old_state.state != new_state or \ - old_state.attributes != attributes: + if old_state['state'] != new_state or \ + old_state['attributes'] != attributes: - self.states[category] = State(new_state, datetime.now(), - attributes) + self.states[category] = create_state(new_state, attributes) self.eventbus.fire(EVENT_STATE_CHANGED, {'category':category, @@ -219,16 +239,17 @@ class StateMachine(object): state = self.states.get(category, None) - return state and state.state == state + return state and state['state'] == state def get_state(self, category): - """ Returns a tuple (state,last_changed) describing + """ Returns a dict (state,last_changed, attributes) describing the state of the specified category. """ if category not in self.states: raise CategoryDoesNotExistException( "Category {} does not exist.".format(category)) - return self.states[category] + # Make a copy so people won't accidently mutate the state + return dict(self.states[category]) class Timer(threading.Thread): @@ -269,7 +290,8 @@ class Timer(threading.Thread): last_fired_on_second = now.second - self.eventbus.fire(EVENT_TIME_CHANGED, {'now':now}) + self.eventbus.fire(EVENT_TIME_CHANGED, + {'now': datetime_to_str(now)}) class HomeAssistantException(Exception): """ General Home Assistant exception occured. """ diff --git a/homeassistant/actors.py b/homeassistant/actors.py index b9e06a3ede8..d4a1f11b250 100644 --- a/homeassistant/actors.py +++ b/homeassistant/actors.py @@ -173,8 +173,8 @@ class LightTrigger(object): """ Returns the datetime object representing the next sun setting. """ state = self.statemachine.get_state(STATE_CATEGORY_SUN) - return util.str_to_datetime( - state.attributes[STATE_ATTRIBUTE_NEXT_SUN_SETTING]) + return ha.str_to_datetime( + state['attributes'][STATE_ATTRIBUTE_NEXT_SUN_SETTING]) def _time_for_light_before_sun_set(self): """ Helper method to calculate the point in time we have to start diff --git a/homeassistant/httpinterface.py b/homeassistant/httpinterface.py index d9284068831..2e65ef37058 100644 --- a/homeassistant/httpinterface.py +++ b/homeassistant/httpinterface.py @@ -32,8 +32,7 @@ import logging from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer from urlparse import urlparse, parse_qs -import homeassistant -import homeassistant.util as util +import homeassistant as ha SERVER_PORT = 8123 @@ -66,8 +65,7 @@ class HTTPInterface(threading.Thread): self.server.statemachine = statemachine self.server.api_password = api_password - eventbus.listen_once(homeassistant.EVENT_START, - lambda event: self.start()) + eventbus.listen_once(ha.EVENT_START, lambda event: self.start()) def run(self): """ Start the HTTP interface. """ @@ -122,15 +120,15 @@ class RequestHandler(BaseHTTPRequestHandler): state = self.server.statemachine.get_state(category) attributes = "
".join( - ["{}: {}".format(attr, state.attributes[attr]) - for attr in state.attributes]) + ["{}: {}".format(attr, state['attributes'][attr]) + for attr in state['attributes']]) write(("" "{}{}{}{}" ""). format(category, - state.state, - util.datetime_to_str(state.last_changed), + state['state'], + ha.datetime_to_str(state['last_changed']), attributes)) write("") @@ -211,14 +209,16 @@ class RequestHandler(BaseHTTPRequestHandler): state = self.server.statemachine.get_state(category) - self._response(use_json, - "State of {}".format(category), - json_data={'category': category, - 'state': state.state, - 'last_changed': - util.datetime_to_str(state.last_changed), - 'attributes': state.attributes - }) + state['category'] = category + + state['last_changed'] = ha.datetime_to_str( + state['last_changed']) + + + print state + + self._response(use_json, "State of {}".format(category), + json_data=state) except KeyError: diff --git a/homeassistant/observers.py b/homeassistant/observers.py index c1468633cd8..0c030ad43fe 100644 --- a/homeassistant/observers.py +++ b/homeassistant/observers.py @@ -18,7 +18,6 @@ import json import requests import homeassistant as ha -import homeassistant.util as util STATE_CATEGORY_SUN = "weather.sun" STATE_ATTRIBUTE_NEXT_SUN_RISING = "next_rising" @@ -77,8 +76,8 @@ def track_sun(eventbus, statemachine, latitude, longitude): format(new_state, next_change.strftime("%H:%M"))) state_attributes = { - STATE_ATTRIBUTE_NEXT_SUN_RISING: util.datetime_to_str(next_rising), - STATE_ATTRIBUTE_NEXT_SUN_SETTING: util.datetime_to_str(next_setting) + STATE_ATTRIBUTE_NEXT_SUN_RISING: ha.datetime_to_str(next_rising), + STATE_ATTRIBUTE_NEXT_SUN_SETTING: ha.datetime_to_str(next_setting) } statemachine.set_state(STATE_CATEGORY_SUN, new_state, state_attributes) @@ -203,7 +202,7 @@ class DeviceTracker(object): DEVICE_STATE_NOT_HOME) # Get the currently used statuses - states_of_devices = [self.statemachine.get_state(category).state + states_of_devices = [self.statemachine.get_state(category)['state'] for category in self.device_state_categories()] # Update the all devices category diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 9e2c7a5381f..93ab356d131 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -4,6 +4,9 @@ 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 @@ -12,9 +15,8 @@ import json import requests -import homeassistant +import homeassistant as ha import homeassistant.httpinterface as httpinterface -import homeassistant.util as util def _setup_call_api(host, port, base_path, api_password): """ Helper method to setup a call api method. """ @@ -33,13 +35,13 @@ def _setup_call_api(host, port, base_path, api_password): return _call_api -class EventBus(homeassistant.EventBus): +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): - homeassistant.EventBus.__init__(self) + ha.EventBus.__init__(self) self._call_api = _setup_call_api(host, port, "event/", api_password) @@ -55,7 +57,15 @@ class EventBus(homeassistant.EventBus): 'event_data': json.dumps(event_data)} try: - self._call_api("fire", data) + req = self._call_api("fire", 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") @@ -73,13 +83,13 @@ class EventBus(homeassistant.EventBus): raise NotImplementedError -class StateMachine(homeassistant.StateMachine): +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): - homeassistant.StateMachine.__init__(self, None) + ha.StateMachine.__init__(self, None) self._call_api = _setup_call_api(host, port, "state/", api_password) @@ -103,6 +113,10 @@ class StateMachine(homeassistant.StateMachine): 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. @@ -117,17 +131,24 @@ class StateMachine(homeassistant.StateMachine): 'attributes': json.dumps(attributes)} try: - self._call_api('change', data) + req = self._call_api('change', data) + + if req.status_code != 200: + error = "Error changing state: {} - {}".format( + req.status_code, req.text) + + self.logger.error("StateMachine:{}".format(error)) + raise ha.HomeAssistantException(error) except requests.exceptions.ConnectionError: - # Raise a Home Assistant error?? 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 tuple (state,last_changed) describing + """ Returns a dict (state,last_changed, attributes) describing the state of the specified category. """ try: @@ -135,13 +156,20 @@ class StateMachine(homeassistant.StateMachine): data = req.json() - return homeassistant.State(data['state'], - util.str_to_datetime(data['last_changed']), - data['attributes']) + return ha.create_state(data['state'], + data['attributes'], + ha.str_to_datetime(data['last_changed'])) 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") - return [] + 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)) diff --git a/homeassistant/test.py b/homeassistant/test.py index f58f64be21c..80164bc897f 100644 --- a/homeassistant/test.py +++ b/homeassistant/test.py @@ -14,7 +14,7 @@ import requests import homeassistant as ha import homeassistant.remote as remote import homeassistant.httpinterface as httpinterface -import homeassistant.util as util + API_PASSWORD = "test1234" @@ -65,7 +65,7 @@ class TestHTTPInterface(unittest.TestCase): "new_state":"debug_state_change", "api_password":API_PASSWORD}) - self.assertEqual(self.statemachine.get_state("test").state, + self.assertEqual(self.statemachine.get_state("test")['state'], "debug_state_change") @@ -102,13 +102,13 @@ class TestHTTPInterface(unittest.TestCase): data = req.json() state = self.statemachine.get_state("test") - trunc_last_changed = state.last_changed.replace(microsecond=0) + self.assertEqual(data['category'], "test") - self.assertEqual(data['state'], state.state) - self.assertEqual(util.str_to_datetime(data['last_changed']), - trunc_last_changed) - self.assertEqual(data['attributes'], state.attributes) + self.assertEqual(data['state'], state['state']) + self.assertEqual(ha.str_to_datetime(data['last_changed']), + state['last_changed']) + self.assertEqual(data['attributes'], state['attributes']) def test_api_state_change(self): @@ -121,7 +121,7 @@ class TestHTTPInterface(unittest.TestCase): "new_state":"debug_state_change2", "api_password":API_PASSWORD}) - self.assertEqual(self.statemachine.get_state("test").state, + self.assertEqual(self.statemachine.get_state("test")['state'], "debug_state_change2") @@ -138,11 +138,10 @@ class TestHTTPInterface(unittest.TestCase): remote_state = self.remote_sm.get_state("test") state = self.statemachine.get_state("test") - trunc_last_changed = state.last_changed.replace(microsecond=0) - self.assertEqual(remote_state.state, state.state) - self.assertEqual(remote_state.last_changed, trunc_last_changed) - self.assertEqual(remote_state.attributes, state.attributes) + self.assertEqual(remote_state['state'], state['state']) + self.assertEqual(remote_state['last_changed'], state['last_changed']) + self.assertEqual(remote_state['attributes'], state['attributes']) def test_remote_sm_state_change(self): @@ -152,8 +151,8 @@ class TestHTTPInterface(unittest.TestCase): state = self.statemachine.get_state("test") - self.assertEqual(state.state, "set_remotely") - self.assertEqual(state.attributes['test'], 1) + self.assertEqual(state['state'], "set_remotely") + self.assertEqual(state['attributes']['test'], 1) def test_api_multiple_state_change(self): @@ -167,9 +166,9 @@ class TestHTTPInterface(unittest.TestCase): "new_state": ["test_state_1", "test_state_2"], "api_password":API_PASSWORD}) - self.assertEqual(self.statemachine.get_state("test").state, + self.assertEqual(self.statemachine.get_state("test")['state'], "test_state_1") - self.assertEqual(self.statemachine.get_state("test2").state, + self.assertEqual(self.statemachine.get_state("test2")['state'], "test_state_2") # pylint: disable=invalid-name @@ -185,7 +184,7 @@ class TestHTTPInterface(unittest.TestCase): "api_password":API_PASSWORD}) cur_state = (self.statemachine. - get_state("test_category_that_does_not_exist").state) + get_state("test_category_that_does_not_exist")['state']) self.assertEqual(req.status_code, 200) self.assertEqual(cur_state, new_state) diff --git a/homeassistant/util.py b/homeassistant/util.py index ba11542b682..51e9ebcef9a 100644 --- a/homeassistant/util.py +++ b/homeassistant/util.py @@ -1,18 +1,8 @@ """ Helper methods for various modules. """ -from datetime import datetime import re -DATE_STR_FORMAT = "%H:%M:%S %d-%m-%Y" - def sanitize_filename(filename): """ Sanitizes a filename by removing .. / and \\. """ return re.sub(r"(~|(\.\.)|/|\+)", "", filename) -def datetime_to_str(dattim): - """ Converts datetime to a string format. """ - return dattim.strftime(DATE_STR_FORMAT) - -def str_to_datetime(dt_str): - """ Converts a string to a datetime object. """ - return datetime.strptime(dt_str, DATE_STR_FORMAT)