From 44293a37388aef33d1bb0c67b87a29c74b183a50 Mon Sep 17 00:00:00 2001 From: adrian-vlad Date: Wed, 24 Feb 2021 15:26:05 +0200 Subject: [PATCH] Add enable and disable services for recorder (#45778) --- homeassistant/components/recorder/__init__.py | 30 ++++ .../components/recorder/services.yaml | 6 + tests/components/recorder/test_init.py | 157 +++++++++++++++++- 3 files changed, 191 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 915e6b45181..3935aa97eb8 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -48,6 +48,8 @@ from .util import ( _LOGGER = logging.getLogger(__name__) SERVICE_PURGE = "purge" +SERVICE_ENABLE = "enable" +SERVICE_DISABLE = "disable" ATTR_KEEP_DAYS = "keep_days" ATTR_REPACK = "repack" @@ -58,6 +60,8 @@ SERVICE_PURGE_SCHEMA = vol.Schema( vol.Optional(ATTR_REPACK, default=False): cv.boolean, } ) +SERVICE_ENABLE_SCHEMA = vol.Schema({}) +SERVICE_DISABLE_SCHEMA = vol.Schema({}) DEFAULT_URL = "sqlite:///{hass_config_path}" DEFAULT_DB_FILE = "home-assistant_v2.db" @@ -199,6 +203,23 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, SERVICE_PURGE, async_handle_purge_service, schema=SERVICE_PURGE_SCHEMA ) + async def async_handle_enable_sevice(service): + instance.set_enable(True) + + hass.services.async_register( + DOMAIN, SERVICE_ENABLE, async_handle_enable_sevice, schema=SERVICE_ENABLE_SCHEMA + ) + + async def async_handle_disable_service(service): + instance.set_enable(False) + + hass.services.async_register( + DOMAIN, + SERVICE_DISABLE, + async_handle_disable_service, + schema=SERVICE_DISABLE_SCHEMA, + ) + return await instance.async_db_ready @@ -255,6 +276,12 @@ class Recorder(threading.Thread): self.get_session = None self._completed_database_setup = None + self.enabled = True + + def set_enable(self, enable): + """Enable or disable recording events and states.""" + self.enabled = enable + @callback def async_initialize(self): """Initialize the recorder.""" @@ -413,6 +440,9 @@ class Recorder(threading.Thread): self._commit_event_session_or_recover() return + if not self.enabled: + return + try: if event.event_type == EVENT_STATE_CHANGED: dbevent = Events.from_event(event, event_data="{}") diff --git a/homeassistant/components/recorder/services.yaml b/homeassistant/components/recorder/services.yaml index e3dea47f4f8..2be5b0e095e 100644 --- a/homeassistant/components/recorder/services.yaml +++ b/homeassistant/components/recorder/services.yaml @@ -24,3 +24,9 @@ purge: default: false selector: boolean: + +disable: + description: Stop the recording of events and state changes + +enabled: + description: Start the recording of events and state changes diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index dfa65944811..63f4b9887c6 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -8,13 +8,17 @@ from sqlalchemy.exc import OperationalError from homeassistant.components.recorder import ( CONF_DB_URL, CONFIG_SCHEMA, + DATA_INSTANCE, DOMAIN, + SERVICE_DISABLE, + SERVICE_ENABLE, + SERVICE_PURGE, + SQLITE_URL_PREFIX, Recorder, run_information, run_information_from_instance, run_information_with_session, ) -from homeassistant.components.recorder.const import DATA_INSTANCE, SQLITE_URL_PREFIX from homeassistant.components.recorder.models import Events, RecorderRuns, States from homeassistant.components.recorder.util import session_scope from homeassistant.const import ( @@ -24,7 +28,7 @@ from homeassistant.const import ( STATE_UNLOCKED, ) from homeassistant.core import Context, CoreState, callback -from homeassistant.setup import async_setup_component +from homeassistant.setup import async_setup_component, setup_component from homeassistant.util import dt as dt_util from .common import async_wait_recording_done, corrupt_db_file, wait_recording_done @@ -518,6 +522,155 @@ def test_run_information(hass_recorder): assert run_info.closed_incorrect is False +def test_has_services(hass_recorder): + """Test the services exist.""" + hass = hass_recorder() + + assert hass.services.has_service(DOMAIN, SERVICE_DISABLE) + assert hass.services.has_service(DOMAIN, SERVICE_ENABLE) + assert hass.services.has_service(DOMAIN, SERVICE_PURGE) + + +def test_service_disable_events_not_recording(hass, hass_recorder): + """Test that events are not recorded when recorder is disabled using service.""" + hass = hass_recorder() + + assert hass.services.call( + DOMAIN, + SERVICE_DISABLE, + {}, + blocking=True, + ) + + event_type = "EVENT_TEST" + + events = [] + + @callback + def event_listener(event): + """Record events from eventbus.""" + if event.event_type == event_type: + events.append(event) + + hass.bus.listen(MATCH_ALL, event_listener) + + event_data1 = {"test_attr": 5, "test_attr_10": "nice"} + hass.bus.fire(event_type, event_data1) + wait_recording_done(hass) + + assert len(events) == 1 + event = events[0] + + with session_scope(hass=hass) as session: + db_events = list(session.query(Events).filter_by(event_type=event_type)) + assert len(db_events) == 0 + + assert hass.services.call( + DOMAIN, + SERVICE_ENABLE, + {}, + blocking=True, + ) + + event_data2 = {"attr_one": 5, "attr_two": "nice"} + hass.bus.fire(event_type, event_data2) + wait_recording_done(hass) + + assert len(events) == 2 + assert events[0] != events[1] + assert events[0].data != events[1].data + + with session_scope(hass=hass) as session: + db_events = list(session.query(Events).filter_by(event_type=event_type)) + assert len(db_events) == 1 + db_event = db_events[0].to_native() + + event = events[1] + + assert event.event_type == db_event.event_type + assert event.data == db_event.data + assert event.origin == db_event.origin + assert event.time_fired.replace(microsecond=0) == db_event.time_fired.replace( + microsecond=0 + ) + + +def test_service_disable_states_not_recording(hass, hass_recorder): + """Test that state changes are not recorded when recorder is disabled using service.""" + hass = hass_recorder() + + assert hass.services.call( + DOMAIN, + SERVICE_DISABLE, + {}, + blocking=True, + ) + + hass.states.set("test.one", "on", {}) + wait_recording_done(hass) + + with session_scope(hass=hass) as session: + assert len(list(session.query(States))) == 0 + + assert hass.services.call( + DOMAIN, + SERVICE_ENABLE, + {}, + blocking=True, + ) + + hass.states.set("test.two", "off", {}) + wait_recording_done(hass) + + with session_scope(hass=hass) as session: + db_states = list(session.query(States)) + assert len(db_states) == 1 + assert db_states[0].event_id > 0 + assert db_states[0].to_native() == _state_empty_context(hass, "test.two") + + +def test_service_disable_run_information_recorded(tmpdir): + """Test that runs are still recorded when recorder is disabled.""" + test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db") + dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" + + hass = get_test_home_assistant() + setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}}) + hass.start() + wait_recording_done(hass) + + with session_scope(hass=hass) as session: + db_run_info = list(session.query(RecorderRuns)) + assert len(db_run_info) == 1 + assert db_run_info[0].start is not None + assert db_run_info[0].end is None + + assert hass.services.call( + DOMAIN, + SERVICE_DISABLE, + {}, + blocking=True, + ) + + wait_recording_done(hass) + hass.stop() + + hass = get_test_home_assistant() + setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}}) + hass.start() + wait_recording_done(hass) + + with session_scope(hass=hass) as session: + db_run_info = list(session.query(RecorderRuns)) + assert len(db_run_info) == 2 + assert db_run_info[0].start is not None + assert db_run_info[0].end is not None + assert db_run_info[1].start is not None + assert db_run_info[1].end is None + + hass.stop() + + class CannotSerializeMe: """A class that the JSONEncoder cannot serialize."""