mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Events and States will now only use JSON serializable attributes
This commit is contained in:
parent
1da1713d2f
commit
83d878810e
@ -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. """
|
||||
|
@ -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
|
||||
|
@ -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 = "<br>".join(
|
||||
["{}: {}".format(attr, state.attributes[attr])
|
||||
for attr in state.attributes])
|
||||
["{}: {}".format(attr, state['attributes'][attr])
|
||||
for attr in state['attributes']])
|
||||
|
||||
write(("<tr>"
|
||||
"<td>{}</td><td>{}</td><td>{}</td><td>{}</td>"
|
||||
"</tr>").
|
||||
format(category,
|
||||
state.state,
|
||||
util.datetime_to_str(state.last_changed),
|
||||
state['state'],
|
||||
ha.datetime_to_str(state['last_changed']),
|
||||
attributes))
|
||||
|
||||
write("</table>")
|
||||
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user