diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index ad8f21f069b..b538a62d008 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -204,11 +204,12 @@ class APIEntityStateView(HomeAssistantView): return self.json_message('No state specified', HTTP_BAD_REQUEST) attributes = request.json.get('attributes') + force_update = request.json.get('force_update', False) is_new_state = self.hass.states.get(entity_id) is None # 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 resp = self.json(self.hass.states.get(entity_id)) diff --git a/homeassistant/core.py b/homeassistant/core.py index ffaccdeae43..d3eed6ce5e0 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -456,7 +456,7 @@ class StateMachine(object): 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. 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) 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 if same_state and same_attr: diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index e4ccf11e168..d120a3b2cf6 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -125,6 +125,15 @@ class Entity(object): """Return True if unable to access real state of the entity.""" 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): """Retrieve latest state.""" pass @@ -190,7 +199,8 @@ class Entity(object): state, attr[ATTR_UNIT_OF_MEASUREMENT]) 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): """Helper method to populate attributes based on properties.""" diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 4bfb01890cf..b2dfc3ae18f 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -259,9 +259,9 @@ class StateMachine(ha.StateMachine): """ 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.""" - set_state(self._api, entity_id, new_state, attributes) + set_state(self._api, entity_id, new_state, attributes, force_update) def mirror(self): """Discard current data and mirrors the remote state machine.""" @@ -450,7 +450,7 @@ def remove_state(api, entity_id): 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. Return True if success. @@ -458,7 +458,8 @@ def set_state(api, entity_id, new_state, attributes=None): attributes = attributes or {} data = {'state': new_state, - 'attributes': attributes} + 'attributes': attributes, + 'force_update': force_update} try: req = api(METHOD_POST, diff --git a/tests/components/test_api.py b/tests/components/test_api.py index 60ff19d4a43..8d1ee1c4ad5 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -136,6 +136,27 @@ class TestAPI(unittest.TestCase): 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 def test_api_fire_event_with_no_data(self): """Test if the API allows us to fire an event.""" diff --git a/tests/test_core.py b/tests/test_core.py index 4930bcef6ed..cb698cdc53c 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -334,6 +334,20 @@ class TestStateMachine(unittest.TestCase): self.assertEqual(state.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): """Test ServiceCall class.""" diff --git a/tests/test_remote.py b/tests/test_remote.py index 58b2f9b359d..893f02bea31 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -8,7 +8,7 @@ import homeassistant.core as ha import homeassistant.bootstrap as bootstrap import homeassistant.remote as remote 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 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')) + 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): """Test Python API is_state.""" self.assertTrue(