Push State (#2365)

* Add ability to push state changes

* Add tests for push state changes

* Fix style issues

* Use better name to force an update
This commit is contained in:
Philip Lundrigan 2016-06-26 01:33:23 -06:00 committed by Paulus Schoutsen
parent 446f998759
commit d13cc227cc
7 changed files with 72 additions and 9 deletions

View File

@ -204,11 +204,12 @@ class APIEntityStateView(HomeAssistantView):
return self.json_message('No state specified', HTTP_BAD_REQUEST) return self.json_message('No state specified', HTTP_BAD_REQUEST)
attributes = request.json.get('attributes') attributes = request.json.get('attributes')
force_update = request.json.get('force_update', False)
is_new_state = self.hass.states.get(entity_id) is None is_new_state = self.hass.states.get(entity_id) is None
# Write state # Write state
self.hass.states.set(entity_id, new_state, attributes) self.hass.states.set(entity_id, new_state, attributes, force_update)
# Read the state back for our response # Read the state back for our response
resp = self.json(self.hass.states.get(entity_id)) resp = self.json(self.hass.states.get(entity_id))

View File

@ -456,7 +456,7 @@ class StateMachine(object):
return True return True
def set(self, entity_id, new_state, attributes=None): def set(self, entity_id, new_state, attributes=None, force_update=False):
"""Set the state of an entity, add entity if it does not exist. """Set the state of an entity, add entity if it does not exist.
Attributes is an optional dict to specify attributes of this state. Attributes is an optional dict to specify attributes of this state.
@ -472,7 +472,8 @@ class StateMachine(object):
old_state = self._states.get(entity_id) old_state = self._states.get(entity_id)
is_existing = old_state is not None is_existing = old_state is not None
same_state = is_existing and old_state.state == new_state same_state = (is_existing and old_state.state == new_state and
not force_update)
same_attr = is_existing and old_state.attributes == attributes same_attr = is_existing and old_state.attributes == attributes
if same_state and same_attr: if same_state and same_attr:

View File

@ -125,6 +125,15 @@ class Entity(object):
"""Return True if unable to access real state of the entity.""" """Return True if unable to access real state of the entity."""
return False return False
@property
def force_update(self):
"""Return True if state updates should be forced.
If True, a state change will be triggered anytime the state property is
updated, not just when the value changes.
"""
return False
def update(self): def update(self):
"""Retrieve latest state.""" """Retrieve latest state."""
pass pass
@ -190,7 +199,8 @@ class Entity(object):
state, attr[ATTR_UNIT_OF_MEASUREMENT]) state, attr[ATTR_UNIT_OF_MEASUREMENT])
state = str(state) state = str(state)
return self.hass.states.set(self.entity_id, state, attr) return self.hass.states.set(
self.entity_id, state, attr, self.force_update)
def _attr_setter(self, name, typ, attr, attrs): def _attr_setter(self, name, typ, attr, attrs):
"""Helper method to populate attributes based on properties.""" """Helper method to populate attributes based on properties."""

View File

@ -259,9 +259,9 @@ class StateMachine(ha.StateMachine):
""" """
return remove_state(self._api, entity_id) return remove_state(self._api, entity_id)
def set(self, entity_id, new_state, attributes=None): def set(self, entity_id, new_state, attributes=None, force_update=False):
"""Call set_state on remote API.""" """Call set_state on remote API."""
set_state(self._api, entity_id, new_state, attributes) set_state(self._api, entity_id, new_state, attributes, force_update)
def mirror(self): def mirror(self):
"""Discard current data and mirrors the remote state machine.""" """Discard current data and mirrors the remote state machine."""
@ -450,7 +450,7 @@ def remove_state(api, entity_id):
return False return False
def set_state(api, entity_id, new_state, attributes=None): def set_state(api, entity_id, new_state, attributes=None, force_update=False):
"""Tell API to update state for entity_id. """Tell API to update state for entity_id.
Return True if success. Return True if success.
@ -458,7 +458,8 @@ def set_state(api, entity_id, new_state, attributes=None):
attributes = attributes or {} attributes = attributes or {}
data = {'state': new_state, data = {'state': new_state,
'attributes': attributes} 'attributes': attributes,
'force_update': force_update}
try: try:
req = api(METHOD_POST, req = api(METHOD_POST,

View File

@ -136,6 +136,27 @@ class TestAPI(unittest.TestCase):
self.assertEqual(400, req.status_code) self.assertEqual(400, req.status_code)
# pylint: disable=invalid-name
def test_api_state_change_push(self):
"""Test if we can push a change the state of an entity."""
hass.states.set("test.test", "not_to_be_set")
events = []
hass.bus.listen(const.EVENT_STATE_CHANGED, events.append)
requests.post(_url(const.URL_API_STATES_ENTITY.format("test.test")),
data=json.dumps({"state": "not_to_be_set"}),
headers=HA_HEADERS)
hass.bus._pool.block_till_done()
self.assertEqual(0, len(events))
requests.post(_url(const.URL_API_STATES_ENTITY.format("test.test")),
data=json.dumps({"state": "not_to_be_set",
"force_update": True}),
headers=HA_HEADERS)
hass.bus._pool.block_till_done()
self.assertEqual(1, len(events))
# pylint: disable=invalid-name # pylint: disable=invalid-name
def test_api_fire_event_with_no_data(self): def test_api_fire_event_with_no_data(self):
"""Test if the API allows us to fire an event.""" """Test if the API allows us to fire an event."""

View File

@ -334,6 +334,20 @@ class TestStateMachine(unittest.TestCase):
self.assertEqual(state.last_changed, self.assertEqual(state.last_changed,
self.states.get('light.Bowl').last_changed) self.states.get('light.Bowl').last_changed)
def test_force_update(self):
"""Test force update option."""
self.pool.add_worker()
events = []
self.bus.listen(EVENT_STATE_CHANGED, events.append)
self.states.set('light.bowl', 'on')
self.bus._pool.block_till_done()
self.assertEqual(0, len(events))
self.states.set('light.bowl', 'on', None, True)
self.bus._pool.block_till_done()
self.assertEqual(1, len(events))
class TestServiceCall(unittest.TestCase): class TestServiceCall(unittest.TestCase):
"""Test ServiceCall class.""" """Test ServiceCall class."""

View File

@ -8,7 +8,7 @@ import homeassistant.core as ha
import homeassistant.bootstrap as bootstrap import homeassistant.bootstrap as bootstrap
import homeassistant.remote as remote import homeassistant.remote as remote
import homeassistant.components.http as http import homeassistant.components.http as http
from homeassistant.const import HTTP_HEADER_HA_AUTH from homeassistant.const import HTTP_HEADER_HA_AUTH, EVENT_STATE_CHANGED
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from tests.common import get_test_instance_port, get_test_home_assistant from tests.common import get_test_instance_port, get_test_home_assistant
@ -155,6 +155,21 @@ class TestRemoteMethods(unittest.TestCase):
self.assertFalse(remote.set_state(broken_api, 'test.test', 'set_test')) self.assertFalse(remote.set_state(broken_api, 'test.test', 'set_test'))
def test_set_state_with_push(self):
"""TestPython API set_state with push option."""
events = []
hass.bus.listen(EVENT_STATE_CHANGED, events.append)
remote.set_state(master_api, 'test.test', 'set_test_2')
remote.set_state(master_api, 'test.test', 'set_test_2')
hass.bus._pool.block_till_done()
self.assertEqual(1, len(events))
remote.set_state(
master_api, 'test.test', 'set_test_2', force_update=True)
hass.bus._pool.block_till_done()
self.assertEqual(2, len(events))
def test_is_state(self): def test_is_state(self):
"""Test Python API is_state.""" """Test Python API is_state."""
self.assertTrue( self.assertTrue(