diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index aa57d1f57f7..1a84f222443 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -59,6 +59,9 @@ def setup(hass, config): hass.http.register_path( 'PUT', re.compile(r'/api/states/(?P[a-zA-Z\._0-9]+)'), _handle_post_state_entity) + hass.http.register_path( + 'DELETE', re.compile(r'/api/states/(?P[a-zA-Z\._0-9]+)'), + _handle_delete_state_entity) # /events hass.http.register_path('GET', URL_API_EVENTS, _handle_get_api_events) @@ -224,6 +227,22 @@ def _handle_post_state_entity(handler, path_match, data): location=URL_API_STATES_ENTITY.format(entity_id)) +def _handle_delete_state_entity(handler, path_match, data): + """Handle request to delete an entity from state machine. + + This handles the following paths: + /api/states/ + """ + entity_id = path_match.group('entity_id') + + if handler.server.hass.states.remove(entity_id): + handler.write_json_message( + "Entity not found", HTTP_NOT_FOUND) + else: + handler.write_json_message( + "Entity removed", HTTP_OK) + + def _handle_get_api_events(handler, path_match, data): """ Handles getting overview of event listeners. """ handler.write_json(events_json(handler.server.hass)) @@ -242,6 +261,7 @@ def _handle_api_post_events_event(handler, path_match, event_data): if event_data is not None and not isinstance(event_data, dict): handler.write_json_message( "event_data should be an object", HTTP_UNPROCESSABLE_ENTITY) + return event_origin = ha.EventOrigin.remote diff --git a/homeassistant/core.py b/homeassistant/core.py index 9d4321a39ab..25062952ed0 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -439,7 +439,20 @@ class StateMachine(object): entity_id = entity_id.lower() with self._lock: - return self._states.pop(entity_id, None) is not None + old_state = self._states.pop(entity_id, None) + + if old_state is None: + return False + + event_data = { + 'entity_id': entity_id, + 'old_state': old_state, + 'new_state': None, + } + + self._bus.fire(EVENT_STATE_CHANGED, event_data) + + return True def set(self, entity_id, new_state, attributes=None): """Set the state of an entity, add entity if it does not exist. @@ -469,10 +482,11 @@ class StateMachine(object): state = State(entity_id, new_state, attributes, last_changed) self._states[entity_id] = state - event_data = {'entity_id': entity_id, 'new_state': state} - - if old_state: - event_data['old_state'] = old_state + event_data = { + 'entity_id': entity_id, + 'old_state': old_state, + 'new_state': state, + } self._bus.fire(EVENT_STATE_CHANGED, event_data) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 0f0deac58b1..d602fa5641f 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -34,16 +34,19 @@ def track_state_change(hass, entity_ids, action, from_state=None, if event.data['entity_id'] not in entity_ids: return - if 'old_state' in event.data: - old_state = event.data['old_state'].state - else: + if event.data['old_state'] is None: old_state = None + else: + old_state = event.data['old_state'].state - if _matcher(old_state, from_state) and \ - _matcher(event.data['new_state'].state, to_state): + if event.data['new_state'] is None: + new_state = None + else: + new_state = event.data['new_state'].state + if _matcher(old_state, from_state) and _matcher(new_state, to_state): action(event.data['entity_id'], - event.data.get('old_state'), + event.data['old_state'], event.data['new_state']) hass.bus.listen(EVENT_STATE_CHANGED, state_change_listener) diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 3b47b60365c..6b55622adce 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -247,6 +247,13 @@ class StateMachine(ha.StateMachine): bus.listen(ha.EVENT_STATE_CHANGED, self._state_changed_listener) + def remove(self, entity_id): + """Remove the state of an entity. + + Returns boolean to indicate if an entity was removed. + """ + return remove_state(self._api, entity_id) + def set(self, entity_id, new_state, attributes=None): """ Calls set_state on remote API . """ set_state(self._api, entity_id, new_state, attributes) @@ -258,7 +265,10 @@ class StateMachine(ha.StateMachine): def _state_changed_listener(self, event): """ Listens for state changed events and applies them. """ - self._states[event.data['entity_id']] = event.data['new_state'] + if event.data['new_state'] is None: + self._states.pop(event.data['entity_id'], None) + else: + self._states[event.data['entity_id']] = event.data['new_state'] class JSONEncoder(json.JSONEncoder): @@ -415,6 +425,26 @@ def get_states(api): return [] +def remove_state(api, entity_id): + """Call API to remove state for entity_id. + + Returns True if entity is gone (removed/never existed). + """ + try: + req = api(METHOD_DELETE, URL_API_STATES_ENTITY.format(entity_id)) + + if req.status_code in (200, 404): + return True + + _LOGGER.error("Error removing state: %d - %s", + req.status_code, req.text) + return False + except HomeAssistantError: + _LOGGER.exception("Error removing state") + + return False + + def set_state(api, entity_id, new_state, attributes=None): """ Tells API to update state for entity_id. diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index e1c17ba6c06..d69d0f198f5 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -24,8 +24,6 @@ class TestEventHelpers(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """ things to be run when tests are started. """ self.hass = ha.HomeAssistant() - self.hass.states.set("light.Bowl", "on") - self.hass.states.set("switch.AC", "off") def tearDown(self): # pylint: disable=invalid-name """ Stop down stuff we started. """ @@ -87,7 +85,7 @@ class TestEventHelpers(unittest.TestCase): self.assertEqual(3, len(wildcard_runs)) def test_track_state_change(self): - """ Test track_state_change. """ + """Test track_state_change.""" # 2 lists to track how often our callbacks get called specific_runs = [] wildcard_runs = [] @@ -97,32 +95,48 @@ class TestEventHelpers(unittest.TestCase): 'on', 'off') track_state_change( - self.hass, 'light.Bowl', lambda a, b, c: wildcard_runs.append(1), + self.hass, 'light.Bowl', + lambda _, old_s, new_s: wildcard_runs.append((old_s, new_s)), ha.MATCH_ALL, ha.MATCH_ALL) + # Adding state to state machine + self.hass.states.set("light.Bowl", "on") + self.hass.pool.block_till_done() + self.assertEqual(0, len(specific_runs)) + self.assertEqual(1, len(wildcard_runs)) + self.assertIsNone(wildcard_runs[-1][0]) + self.assertIsNotNone(wildcard_runs[-1][1]) + # Set same state should not trigger a state change/listener self.hass.states.set('light.Bowl', 'on') self.hass.pool.block_till_done() self.assertEqual(0, len(specific_runs)) - self.assertEqual(0, len(wildcard_runs)) + self.assertEqual(1, len(wildcard_runs)) # State change off -> on self.hass.states.set('light.Bowl', 'off') self.hass.pool.block_till_done() self.assertEqual(1, len(specific_runs)) - self.assertEqual(1, len(wildcard_runs)) + self.assertEqual(2, len(wildcard_runs)) # State change off -> off self.hass.states.set('light.Bowl', 'off', {"some_attr": 1}) self.hass.pool.block_till_done() self.assertEqual(1, len(specific_runs)) - self.assertEqual(2, len(wildcard_runs)) + self.assertEqual(3, len(wildcard_runs)) # State change off -> on self.hass.states.set('light.Bowl', 'on') self.hass.pool.block_till_done() self.assertEqual(1, len(specific_runs)) - self.assertEqual(3, len(wildcard_runs)) + self.assertEqual(4, len(wildcard_runs)) + + self.hass.states.remove('light.bowl') + self.hass.pool.block_till_done() + self.assertEqual(1, len(specific_runs)) + self.assertEqual(5, len(wildcard_runs)) + self.assertIsNotNone(wildcard_runs[-1][0]) + self.assertIsNone(wildcard_runs[-1][1]) def test_track_sunrise(self): """ Test track sunrise """ diff --git a/tests/test_core.py b/tests/test_core.py index 6a19a2aa7e8..d1b2221998e 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -20,7 +20,6 @@ import homeassistant.core as ha from homeassistant.exceptions import ( HomeAssistantError, InvalidEntityFormatError) import homeassistant.util.dt as dt_util -from homeassistant.helpers.event import track_state_change from homeassistant.const import ( __version__, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, ATTR_FRIENDLY_NAME, TEMP_CELCIUS, @@ -150,7 +149,7 @@ class TestEventBus(unittest.TestCase): self.bus._pool.add_worker() old_count = len(self.bus.listeners) - listener = lambda x: len + def listener(_): pass self.bus.listen('test', listener) @@ -280,12 +279,26 @@ class TestStateMachine(unittest.TestCase): def test_remove(self): """ Test remove method. """ - self.assertTrue('light.bowl' in self.states.entity_ids()) + self.pool.add_worker() + events = [] + self.bus.listen(EVENT_STATE_CHANGED, + lambda event: events.append(event)) + + self.assertIn('light.bowl', self.states.entity_ids()) self.assertTrue(self.states.remove('light.bowl')) - self.assertFalse('light.bowl' in self.states.entity_ids()) + self.pool.block_till_done() + + self.assertNotIn('light.bowl', self.states.entity_ids()) + self.assertEqual(1, len(events)) + self.assertEqual('light.bowl', events[0].data.get('entity_id')) + self.assertIsNotNone(events[0].data.get('old_state')) + self.assertEqual('light.bowl', events[0].data['old_state'].entity_id) + self.assertIsNone(events[0].data.get('new_state')) # If it does not exist, we should get False self.assertFalse(self.states.remove('light.Bowl')) + self.pool.block_till_done() + self.assertEqual(1, len(events)) def test_case_insensitivty(self): self.pool.add_worker() diff --git a/tests/test_remote.py b/tests/test_remote.py index 777f1c30e84..bf6a916f22c 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -135,9 +135,17 @@ class TestRemoteMethods(unittest.TestCase): self.assertEqual(hass.states.all(), remote.get_states(master_api)) self.assertEqual([], remote.get_states(broken_api)) + def test_remove_state(self): + """ Test Python API set_state. """ + hass.states.set('test.remove_state', 'set_test') + + self.assertIn('test.remove_state', hass.states.entity_ids()) + remote.remove_state(master_api, 'test.remove_state') + self.assertNotIn('test.remove_state', hass.states.entity_ids()) + def test_set_state(self): """ Test Python API set_state. """ - hass.states.set('test.test', 'set_test') + remote.set_state(master_api, 'test.test', 'set_test') state = hass.states.get('test.test') @@ -225,6 +233,29 @@ class TestRemoteClasses(unittest.TestCase): self.assertEqual("remote.statemachine test", slave.states.get("remote.test").state) + def test_statemachine_remove_from_master(self): + hass.states.set("remote.master_remove", "remove me!") + hass.pool.block_till_done() + + self.assertIn('remote.master_remove', slave.states.entity_ids()) + + hass.states.remove("remote.master_remove") + hass.pool.block_till_done() + + self.assertNotIn('remote.master_remove', slave.states.entity_ids()) + + def test_statemachine_remove_from_slave(self): + hass.states.set("remote.slave_remove", "remove me!") + hass.pool.block_till_done() + + self.assertIn('remote.slave_remove', slave.states.entity_ids()) + + self.assertTrue(slave.states.remove("remote.slave_remove")) + slave.pool.block_till_done() + hass.pool.block_till_done() + + self.assertNotIn('remote.slave_remove', slave.states.entity_ids()) + def test_eventbus_fire(self): """ Test if events fired from the eventbus get fired. """ test_value = []