mirror of
https://github.com/home-assistant/core.git
synced 2025-07-20 11:47:06 +00:00
Merge pull request #1246 from balloob/feature/remove-state-fire-event
Have remove state fire state_changed event
This commit is contained in:
commit
dabb8d5bbc
@ -59,6 +59,9 @@ def setup(hass, config):
|
|||||||
hass.http.register_path(
|
hass.http.register_path(
|
||||||
'PUT', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
'PUT', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||||
_handle_post_state_entity)
|
_handle_post_state_entity)
|
||||||
|
hass.http.register_path(
|
||||||
|
'DELETE', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||||
|
_handle_delete_state_entity)
|
||||||
|
|
||||||
# /events
|
# /events
|
||||||
hass.http.register_path('GET', URL_API_EVENTS, _handle_get_api_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))
|
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>
|
||||||
|
"""
|
||||||
|
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):
|
def _handle_get_api_events(handler, path_match, data):
|
||||||
""" Handles getting overview of event listeners. """
|
""" Handles getting overview of event listeners. """
|
||||||
handler.write_json(events_json(handler.server.hass))
|
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):
|
if event_data is not None and not isinstance(event_data, dict):
|
||||||
handler.write_json_message(
|
handler.write_json_message(
|
||||||
"event_data should be an object", HTTP_UNPROCESSABLE_ENTITY)
|
"event_data should be an object", HTTP_UNPROCESSABLE_ENTITY)
|
||||||
|
return
|
||||||
|
|
||||||
event_origin = ha.EventOrigin.remote
|
event_origin = ha.EventOrigin.remote
|
||||||
|
|
||||||
|
@ -439,7 +439,20 @@ class StateMachine(object):
|
|||||||
entity_id = entity_id.lower()
|
entity_id = entity_id.lower()
|
||||||
|
|
||||||
with self._lock:
|
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):
|
def set(self, entity_id, new_state, attributes=None):
|
||||||
"""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.
|
||||||
@ -469,10 +482,11 @@ class StateMachine(object):
|
|||||||
state = State(entity_id, new_state, attributes, last_changed)
|
state = State(entity_id, new_state, attributes, last_changed)
|
||||||
self._states[entity_id] = state
|
self._states[entity_id] = state
|
||||||
|
|
||||||
event_data = {'entity_id': entity_id, 'new_state': state}
|
event_data = {
|
||||||
|
'entity_id': entity_id,
|
||||||
if old_state:
|
'old_state': old_state,
|
||||||
event_data['old_state'] = old_state
|
'new_state': state,
|
||||||
|
}
|
||||||
|
|
||||||
self._bus.fire(EVENT_STATE_CHANGED, event_data)
|
self._bus.fire(EVENT_STATE_CHANGED, event_data)
|
||||||
|
|
||||||
|
@ -34,16 +34,19 @@ def track_state_change(hass, entity_ids, action, from_state=None,
|
|||||||
if event.data['entity_id'] not in entity_ids:
|
if event.data['entity_id'] not in entity_ids:
|
||||||
return
|
return
|
||||||
|
|
||||||
if 'old_state' in event.data:
|
if event.data['old_state'] is None:
|
||||||
old_state = event.data['old_state'].state
|
|
||||||
else:
|
|
||||||
old_state = None
|
old_state = None
|
||||||
|
else:
|
||||||
|
old_state = event.data['old_state'].state
|
||||||
|
|
||||||
if _matcher(old_state, from_state) and \
|
if event.data['new_state'] is None:
|
||||||
_matcher(event.data['new_state'].state, to_state):
|
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'],
|
action(event.data['entity_id'],
|
||||||
event.data.get('old_state'),
|
event.data['old_state'],
|
||||||
event.data['new_state'])
|
event.data['new_state'])
|
||||||
|
|
||||||
hass.bus.listen(EVENT_STATE_CHANGED, state_change_listener)
|
hass.bus.listen(EVENT_STATE_CHANGED, state_change_listener)
|
||||||
|
@ -247,6 +247,13 @@ class StateMachine(ha.StateMachine):
|
|||||||
|
|
||||||
bus.listen(ha.EVENT_STATE_CHANGED, self._state_changed_listener)
|
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):
|
def set(self, entity_id, new_state, attributes=None):
|
||||||
""" Calls set_state on remote API . """
|
""" Calls set_state on remote API . """
|
||||||
set_state(self._api, entity_id, new_state, attributes)
|
set_state(self._api, entity_id, new_state, attributes)
|
||||||
@ -258,6 +265,9 @@ class StateMachine(ha.StateMachine):
|
|||||||
|
|
||||||
def _state_changed_listener(self, event):
|
def _state_changed_listener(self, event):
|
||||||
""" Listens for state changed events and applies them. """
|
""" Listens for state changed events and applies them. """
|
||||||
|
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']
|
self._states[event.data['entity_id']] = event.data['new_state']
|
||||||
|
|
||||||
|
|
||||||
@ -415,6 +425,26 @@ def get_states(api):
|
|||||||
return []
|
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):
|
def set_state(api, entity_id, new_state, attributes=None):
|
||||||
"""
|
"""
|
||||||
Tells API to update state for entity_id.
|
Tells API to update state for entity_id.
|
||||||
|
@ -24,8 +24,6 @@ class TestEventHelpers(unittest.TestCase):
|
|||||||
def setUp(self): # pylint: disable=invalid-name
|
def setUp(self): # pylint: disable=invalid-name
|
||||||
""" things to be run when tests are started. """
|
""" things to be run when tests are started. """
|
||||||
self.hass = ha.HomeAssistant()
|
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
|
def tearDown(self): # pylint: disable=invalid-name
|
||||||
""" Stop down stuff we started. """
|
""" Stop down stuff we started. """
|
||||||
@ -87,7 +85,7 @@ class TestEventHelpers(unittest.TestCase):
|
|||||||
self.assertEqual(3, len(wildcard_runs))
|
self.assertEqual(3, len(wildcard_runs))
|
||||||
|
|
||||||
def test_track_state_change(self):
|
def test_track_state_change(self):
|
||||||
""" Test track_state_change. """
|
"""Test track_state_change."""
|
||||||
# 2 lists to track how often our callbacks get called
|
# 2 lists to track how often our callbacks get called
|
||||||
specific_runs = []
|
specific_runs = []
|
||||||
wildcard_runs = []
|
wildcard_runs = []
|
||||||
@ -97,32 +95,48 @@ class TestEventHelpers(unittest.TestCase):
|
|||||||
'on', 'off')
|
'on', 'off')
|
||||||
|
|
||||||
track_state_change(
|
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)
|
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
|
# Set same state should not trigger a state change/listener
|
||||||
self.hass.states.set('light.Bowl', 'on')
|
self.hass.states.set('light.Bowl', 'on')
|
||||||
self.hass.pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
self.assertEqual(0, len(specific_runs))
|
self.assertEqual(0, len(specific_runs))
|
||||||
self.assertEqual(0, len(wildcard_runs))
|
self.assertEqual(1, len(wildcard_runs))
|
||||||
|
|
||||||
# State change off -> on
|
# State change off -> on
|
||||||
self.hass.states.set('light.Bowl', 'off')
|
self.hass.states.set('light.Bowl', 'off')
|
||||||
self.hass.pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
self.assertEqual(1, len(specific_runs))
|
self.assertEqual(1, len(specific_runs))
|
||||||
self.assertEqual(1, len(wildcard_runs))
|
self.assertEqual(2, len(wildcard_runs))
|
||||||
|
|
||||||
# State change off -> off
|
# State change off -> off
|
||||||
self.hass.states.set('light.Bowl', 'off', {"some_attr": 1})
|
self.hass.states.set('light.Bowl', 'off', {"some_attr": 1})
|
||||||
self.hass.pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
self.assertEqual(1, len(specific_runs))
|
self.assertEqual(1, len(specific_runs))
|
||||||
self.assertEqual(2, len(wildcard_runs))
|
self.assertEqual(3, len(wildcard_runs))
|
||||||
|
|
||||||
# State change off -> on
|
# State change off -> on
|
||||||
self.hass.states.set('light.Bowl', 'on')
|
self.hass.states.set('light.Bowl', 'on')
|
||||||
self.hass.pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
self.assertEqual(1, len(specific_runs))
|
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):
|
def test_track_sunrise(self):
|
||||||
""" Test track sunrise """
|
""" Test track sunrise """
|
||||||
|
@ -20,7 +20,6 @@ import homeassistant.core as ha
|
|||||||
from homeassistant.exceptions import (
|
from homeassistant.exceptions import (
|
||||||
HomeAssistantError, InvalidEntityFormatError)
|
HomeAssistantError, InvalidEntityFormatError)
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.helpers.event import track_state_change
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
__version__, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
|
__version__, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
|
||||||
EVENT_STATE_CHANGED, ATTR_FRIENDLY_NAME, TEMP_CELCIUS,
|
EVENT_STATE_CHANGED, ATTR_FRIENDLY_NAME, TEMP_CELCIUS,
|
||||||
@ -150,7 +149,7 @@ class TestEventBus(unittest.TestCase):
|
|||||||
self.bus._pool.add_worker()
|
self.bus._pool.add_worker()
|
||||||
old_count = len(self.bus.listeners)
|
old_count = len(self.bus.listeners)
|
||||||
|
|
||||||
listener = lambda x: len
|
def listener(_): pass
|
||||||
|
|
||||||
self.bus.listen('test', listener)
|
self.bus.listen('test', listener)
|
||||||
|
|
||||||
@ -280,12 +279,26 @@ class TestStateMachine(unittest.TestCase):
|
|||||||
|
|
||||||
def test_remove(self):
|
def test_remove(self):
|
||||||
""" Test remove method. """
|
""" 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.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
|
# If it does not exist, we should get False
|
||||||
self.assertFalse(self.states.remove('light.Bowl'))
|
self.assertFalse(self.states.remove('light.Bowl'))
|
||||||
|
self.pool.block_till_done()
|
||||||
|
self.assertEqual(1, len(events))
|
||||||
|
|
||||||
def test_case_insensitivty(self):
|
def test_case_insensitivty(self):
|
||||||
self.pool.add_worker()
|
self.pool.add_worker()
|
||||||
|
@ -135,9 +135,17 @@ class TestRemoteMethods(unittest.TestCase):
|
|||||||
self.assertEqual(hass.states.all(), remote.get_states(master_api))
|
self.assertEqual(hass.states.all(), remote.get_states(master_api))
|
||||||
self.assertEqual([], remote.get_states(broken_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):
|
def test_set_state(self):
|
||||||
""" Test Python API set_state. """
|
""" 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')
|
state = hass.states.get('test.test')
|
||||||
|
|
||||||
@ -225,6 +233,29 @@ class TestRemoteClasses(unittest.TestCase):
|
|||||||
self.assertEqual("remote.statemachine test",
|
self.assertEqual("remote.statemachine test",
|
||||||
slave.states.get("remote.test").state)
|
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):
|
def test_eventbus_fire(self):
|
||||||
""" Test if events fired from the eventbus get fired. """
|
""" Test if events fired from the eventbus get fired. """
|
||||||
test_value = []
|
test_value = []
|
||||||
|
Loading…
x
Reference in New Issue
Block a user