Events and States will now only use JSON serializable attributes

This commit is contained in:
Paulus Schoutsen 2013-10-27 17:39:54 -07:00
parent 1da1713d2f
commit 83d878810e
7 changed files with 122 additions and 84 deletions

View File

@ -26,7 +26,7 @@ TIMER_INTERVAL = 10 # seconds
# every minute. # every minute.
assert 60 % TIMER_INTERVAL == 0, "60 % TIMER_INTERVAL should be 0!" 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): def start_home_assistant(eventbus):
""" Start home assistant. """ """ Start home assistant. """
@ -41,6 +41,14 @@ def start_home_assistant(eventbus):
except KeyboardInterrupt: except KeyboardInterrupt:
break 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): def ensure_list(parameter):
""" Wraps parameter in a list if it is not one and returns it. """ """ Wraps parameter in a list if it is not one and returns it. """
return parameter if isinstance(parameter, list) else [parameter] return parameter if isinstance(parameter, list) else [parameter]
@ -52,6 +60,18 @@ def matcher(subject, pattern):
""" """
return '*' in pattern or subject in 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): def track_state_change(eventbus, category, from_state, to_state, action):
""" Helper method to track specific state changes. """ """ Helper method to track specific state changes. """
from_state = ensure_list(from_state) from_state = ensure_list(from_state)
@ -60,8 +80,8 @@ def track_state_change(eventbus, category, from_state, to_state, action):
def listener(event): def listener(event):
""" State change listener that listens for specific state changes. """ """ State change listener that listens for specific state changes. """
if category == event.data['category'] and \ if category == event.data['category'] and \
matcher(event.data['old_state'].state, from_state) and \ matcher(event.data['old_state']['state'], from_state) and \
matcher(event.data['new_state'].state, to_state): matcher(event.data['new_state']['state'], to_state):
action(event.data['category'], action(event.data['category'],
event.data['old_state'], event.data['old_state'],
@ -81,21 +101,23 @@ def track_time_change(eventbus, action,
def listener(event): def listener(event):
""" Listens for matching time_changed events. """ """ 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 \ (not point_in_time and \
matcher(event.data['now'].year, year) and \ matcher(now.year, year) and \
matcher(event.data['now'].month, month) and \ matcher(now.month, month) and \
matcher(event.data['now'].day, day) and \ matcher(now.day, day) and \
matcher(event.data['now'].hour, hour) and \ matcher(now.hour, hour) and \
matcher(event.data['now'].minute, minute) and \ matcher(now.minute, minute) and \
matcher(event.data['now'].second, second)): matcher(now.second, second)):
# point_in_time are exact points in time # point_in_time are exact points in time
# so we always remove it after fire # so we always remove it after fire
if listen_once or point_in_time: if listen_once or point_in_time:
event.eventbus.remove_listener(EVENT_TIME_CHANGED, listener) event.eventbus.remove_listener(EVENT_TIME_CHANGED, listener)
action(event.data['now']) action(now)
eventbus.listen(EVENT_TIME_CHANGED, listener) eventbus.listen(EVENT_TIME_CHANGED, listener)
@ -194,18 +216,16 @@ class StateMachine(object):
# Add category if it does not exist # Add category if it does not exist
if category not in self.states: if category not in self.states:
self.states[category] = State(new_state, datetime.now(), self.states[category] = create_state(new_state, attributes)
attributes)
# Change state and fire listeners # Change state and fire listeners
else: else:
old_state = self.states[category] old_state = self.states[category]
if old_state.state != new_state or \ if old_state['state'] != new_state or \
old_state.attributes != attributes: old_state['attributes'] != attributes:
self.states[category] = State(new_state, datetime.now(), self.states[category] = create_state(new_state, attributes)
attributes)
self.eventbus.fire(EVENT_STATE_CHANGED, self.eventbus.fire(EVENT_STATE_CHANGED,
{'category':category, {'category':category,
@ -219,16 +239,17 @@ class StateMachine(object):
state = self.states.get(category, None) state = self.states.get(category, None)
return state and state.state == state return state and state['state'] == state
def get_state(self, category): 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. """ the state of the specified category. """
if category not in self.states: if category not in self.states:
raise CategoryDoesNotExistException( raise CategoryDoesNotExistException(
"Category {} does not exist.".format(category)) "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): class Timer(threading.Thread):
@ -269,7 +290,8 @@ class Timer(threading.Thread):
last_fired_on_second = now.second 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): class HomeAssistantException(Exception):
""" General Home Assistant exception occured. """ """ General Home Assistant exception occured. """

View File

@ -173,8 +173,8 @@ class LightTrigger(object):
""" Returns the datetime object representing the next sun setting. """ """ Returns the datetime object representing the next sun setting. """
state = self.statemachine.get_state(STATE_CATEGORY_SUN) state = self.statemachine.get_state(STATE_CATEGORY_SUN)
return util.str_to_datetime( return ha.str_to_datetime(
state.attributes[STATE_ATTRIBUTE_NEXT_SUN_SETTING]) state['attributes'][STATE_ATTRIBUTE_NEXT_SUN_SETTING])
def _time_for_light_before_sun_set(self): def _time_for_light_before_sun_set(self):
""" Helper method to calculate the point in time we have to start """ Helper method to calculate the point in time we have to start

View File

@ -32,8 +32,7 @@ import logging
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
from urlparse import urlparse, parse_qs from urlparse import urlparse, parse_qs
import homeassistant import homeassistant as ha
import homeassistant.util as util
SERVER_PORT = 8123 SERVER_PORT = 8123
@ -66,8 +65,7 @@ class HTTPInterface(threading.Thread):
self.server.statemachine = statemachine self.server.statemachine = statemachine
self.server.api_password = api_password self.server.api_password = api_password
eventbus.listen_once(homeassistant.EVENT_START, eventbus.listen_once(ha.EVENT_START, lambda event: self.start())
lambda event: self.start())
def run(self): def run(self):
""" Start the HTTP interface. """ """ Start the HTTP interface. """
@ -122,15 +120,15 @@ class RequestHandler(BaseHTTPRequestHandler):
state = self.server.statemachine.get_state(category) state = self.server.statemachine.get_state(category)
attributes = "<br>".join( attributes = "<br>".join(
["{}: {}".format(attr, state.attributes[attr]) ["{}: {}".format(attr, state['attributes'][attr])
for attr in state.attributes]) for attr in state['attributes']])
write(("<tr>" write(("<tr>"
"<td>{}</td><td>{}</td><td>{}</td><td>{}</td>" "<td>{}</td><td>{}</td><td>{}</td><td>{}</td>"
"</tr>"). "</tr>").
format(category, format(category,
state.state, state['state'],
util.datetime_to_str(state.last_changed), ha.datetime_to_str(state['last_changed']),
attributes)) attributes))
write("</table>") write("</table>")
@ -211,14 +209,16 @@ class RequestHandler(BaseHTTPRequestHandler):
state = self.server.statemachine.get_state(category) state = self.server.statemachine.get_state(category)
self._response(use_json, state['category'] = category
"State of {}".format(category),
json_data={'category': category, state['last_changed'] = ha.datetime_to_str(
'state': state.state, state['last_changed'])
'last_changed':
util.datetime_to_str(state.last_changed),
'attributes': state.attributes print state
})
self._response(use_json, "State of {}".format(category),
json_data=state)
except KeyError: except KeyError:

View File

@ -18,7 +18,6 @@ import json
import requests import requests
import homeassistant as ha import homeassistant as ha
import homeassistant.util as util
STATE_CATEGORY_SUN = "weather.sun" STATE_CATEGORY_SUN = "weather.sun"
STATE_ATTRIBUTE_NEXT_SUN_RISING = "next_rising" 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"))) format(new_state, next_change.strftime("%H:%M")))
state_attributes = { state_attributes = {
STATE_ATTRIBUTE_NEXT_SUN_RISING: util.datetime_to_str(next_rising), STATE_ATTRIBUTE_NEXT_SUN_RISING: ha.datetime_to_str(next_rising),
STATE_ATTRIBUTE_NEXT_SUN_SETTING: util.datetime_to_str(next_setting) STATE_ATTRIBUTE_NEXT_SUN_SETTING: ha.datetime_to_str(next_setting)
} }
statemachine.set_state(STATE_CATEGORY_SUN, new_state, state_attributes) statemachine.set_state(STATE_CATEGORY_SUN, new_state, state_attributes)
@ -203,7 +202,7 @@ class DeviceTracker(object):
DEVICE_STATE_NOT_HOME) DEVICE_STATE_NOT_HOME)
# Get the currently used statuses # 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()] for category in self.device_state_categories()]
# Update the all devices category # Update the all devices category

View File

@ -4,6 +4,9 @@ homeassistant.remote
A module containing drop in replacements for core parts that will interface A module containing drop in replacements for core parts that will interface
with a remote instance of home assistant. 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 threading
@ -12,9 +15,8 @@ import json
import requests import requests
import homeassistant import homeassistant as ha
import homeassistant.httpinterface as httpinterface import homeassistant.httpinterface as httpinterface
import homeassistant.util as util
def _setup_call_api(host, port, base_path, api_password): def _setup_call_api(host, port, base_path, api_password):
""" Helper method to setup a call api method. """ """ 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 return _call_api
class EventBus(homeassistant.EventBus): class EventBus(ha.EventBus):
""" Drop-in replacement for a normal eventbus that will forward events to """ Drop-in replacement for a normal eventbus that will forward events to
a remote eventbus. a remote eventbus.
""" """
def __init__(self, host, api_password, port=None): 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) 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)} 'event_data': json.dumps(event_data)}
try: 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: except requests.exceptions.ConnectionError:
self.logger.exception("EventBus:Error connecting to server") self.logger.exception("EventBus:Error connecting to server")
@ -73,13 +83,13 @@ class EventBus(homeassistant.EventBus):
raise NotImplementedError raise NotImplementedError
class StateMachine(homeassistant.StateMachine): class StateMachine(ha.StateMachine):
""" Drop-in replacement for a normal statemachine that communicates with a """ Drop-in replacement for a normal statemachine that communicates with a
remote statemachine. remote statemachine.
""" """
def __init__(self, host, api_password, port=None): 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) 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") self.logger.exception("StateMachine:Got unexpected result")
return [] 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): def set_state(self, category, new_state, attributes=None):
""" Set the state of a category, add category if it does not exist. """ 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)} 'attributes': json.dumps(attributes)}
try: 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: except requests.exceptions.ConnectionError:
# Raise a Home Assistant error??
self.logger.exception("StateMachine:Error connecting to server") self.logger.exception("StateMachine:Error connecting to server")
raise ha.HomeAssistantException("Error connecting to server")
finally: finally:
self.lock.release() self.lock.release()
def get_state(self, category): 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. """ the state of the specified category. """
try: try:
@ -135,13 +156,20 @@ class StateMachine(homeassistant.StateMachine):
data = req.json() data = req.json()
return homeassistant.State(data['state'], return ha.create_state(data['state'],
util.str_to_datetime(data['last_changed']), data['attributes'],
data['attributes']) ha.str_to_datetime(data['last_changed']))
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
self.logger.exception("StateMachine:Error connecting to server") 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 except ValueError: # If req.json() can't parse the json
self.logger.exception("StateMachine:Got unexpected result") 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))

View File

@ -14,7 +14,7 @@ import requests
import homeassistant as ha import homeassistant as ha
import homeassistant.remote as remote import homeassistant.remote as remote
import homeassistant.httpinterface as httpinterface import homeassistant.httpinterface as httpinterface
import homeassistant.util as util
API_PASSWORD = "test1234" API_PASSWORD = "test1234"
@ -65,7 +65,7 @@ class TestHTTPInterface(unittest.TestCase):
"new_state":"debug_state_change", "new_state":"debug_state_change",
"api_password":API_PASSWORD}) "api_password":API_PASSWORD})
self.assertEqual(self.statemachine.get_state("test").state, self.assertEqual(self.statemachine.get_state("test")['state'],
"debug_state_change") "debug_state_change")
@ -102,13 +102,13 @@ class TestHTTPInterface(unittest.TestCase):
data = req.json() data = req.json()
state = self.statemachine.get_state("test") state = self.statemachine.get_state("test")
trunc_last_changed = state.last_changed.replace(microsecond=0)
self.assertEqual(data['category'], "test") self.assertEqual(data['category'], "test")
self.assertEqual(data['state'], state.state) self.assertEqual(data['state'], state['state'])
self.assertEqual(util.str_to_datetime(data['last_changed']), self.assertEqual(ha.str_to_datetime(data['last_changed']),
trunc_last_changed) state['last_changed'])
self.assertEqual(data['attributes'], state.attributes) self.assertEqual(data['attributes'], state['attributes'])
def test_api_state_change(self): def test_api_state_change(self):
@ -121,7 +121,7 @@ class TestHTTPInterface(unittest.TestCase):
"new_state":"debug_state_change2", "new_state":"debug_state_change2",
"api_password":API_PASSWORD}) "api_password":API_PASSWORD})
self.assertEqual(self.statemachine.get_state("test").state, self.assertEqual(self.statemachine.get_state("test")['state'],
"debug_state_change2") "debug_state_change2")
@ -138,11 +138,10 @@ class TestHTTPInterface(unittest.TestCase):
remote_state = self.remote_sm.get_state("test") remote_state = self.remote_sm.get_state("test")
state = self.statemachine.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['state'], state['state'])
self.assertEqual(remote_state.last_changed, trunc_last_changed) self.assertEqual(remote_state['last_changed'], state['last_changed'])
self.assertEqual(remote_state.attributes, state.attributes) self.assertEqual(remote_state['attributes'], state['attributes'])
def test_remote_sm_state_change(self): def test_remote_sm_state_change(self):
@ -152,8 +151,8 @@ class TestHTTPInterface(unittest.TestCase):
state = self.statemachine.get_state("test") state = self.statemachine.get_state("test")
self.assertEqual(state.state, "set_remotely") self.assertEqual(state['state'], "set_remotely")
self.assertEqual(state.attributes['test'], 1) self.assertEqual(state['attributes']['test'], 1)
def test_api_multiple_state_change(self): def test_api_multiple_state_change(self):
@ -167,9 +166,9 @@ class TestHTTPInterface(unittest.TestCase):
"new_state": ["test_state_1", "test_state_2"], "new_state": ["test_state_1", "test_state_2"],
"api_password":API_PASSWORD}) "api_password":API_PASSWORD})
self.assertEqual(self.statemachine.get_state("test").state, self.assertEqual(self.statemachine.get_state("test")['state'],
"test_state_1") "test_state_1")
self.assertEqual(self.statemachine.get_state("test2").state, self.assertEqual(self.statemachine.get_state("test2")['state'],
"test_state_2") "test_state_2")
# pylint: disable=invalid-name # pylint: disable=invalid-name
@ -185,7 +184,7 @@ class TestHTTPInterface(unittest.TestCase):
"api_password":API_PASSWORD}) "api_password":API_PASSWORD})
cur_state = (self.statemachine. 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(req.status_code, 200)
self.assertEqual(cur_state, new_state) self.assertEqual(cur_state, new_state)

View File

@ -1,18 +1,8 @@
""" Helper methods for various modules. """ """ Helper methods for various modules. """
from datetime import datetime
import re import re
DATE_STR_FORMAT = "%H:%M:%S %d-%m-%Y"
def sanitize_filename(filename): def sanitize_filename(filename):
""" Sanitizes a filename by removing .. / and \\. """ """ Sanitizes a filename by removing .. / and \\. """
return re.sub(r"(~|(\.\.)|/|\+)", "", filename) 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)