diff --git a/.strict-typing b/.strict-typing index 2915f398953..f42bd4a4ab1 100644 --- a/.strict-typing +++ b/.strict-typing @@ -188,6 +188,7 @@ homeassistant.components.recorder.pool homeassistant.components.recorder.purge homeassistant.components.recorder.repack homeassistant.components.recorder.run_history +homeassistant.components.recorder.services homeassistant.components.recorder.statistics homeassistant.components.recorder.system_health homeassistant.components.recorder.tasks diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index f9d462abeea..00f12710c18 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -7,7 +7,7 @@ from typing import Any import voluptuous as vol from homeassistant.const import CONF_EXCLUDE, EVENT_STATE_CHANGED -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import ( INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, @@ -17,15 +17,11 @@ from homeassistant.helpers.entityfilter import ( from homeassistant.helpers.integration_platform import ( async_process_integration_platforms, ) -from homeassistant.helpers.service import async_extract_entity_ids from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from . import history, statistics, websocket_api from .const import ( - ATTR_APPLY_FILTER, - ATTR_KEEP_DAYS, - ATTR_REPACK, CONF_DB_INTEGRITY_CHECK, DATA_INSTANCE, DOMAIN, @@ -33,39 +29,12 @@ from .const import ( SQLITE_URL_PREFIX, ) from .core import Recorder +from .services import async_register_services from .tasks import AddRecorderPlatformTask _LOGGER = logging.getLogger(__name__) -SERVICE_PURGE = "purge" -SERVICE_PURGE_ENTITIES = "purge_entities" -SERVICE_ENABLE = "enable" -SERVICE_DISABLE = "disable" - - -SERVICE_PURGE_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_KEEP_DAYS): cv.positive_int, - vol.Optional(ATTR_REPACK, default=False): cv.boolean, - vol.Optional(ATTR_APPLY_FILTER, default=False): cv.boolean, - } -) - -ATTR_DOMAINS = "domains" -ATTR_ENTITY_GLOBS = "entity_globs" - -SERVICE_PURGE_ENTITIES_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_DOMAINS, default=[]): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_ENTITY_GLOBS, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - } -).extend(cv.ENTITY_SERVICE_FIELDS) -SERVICE_ENABLE_SCHEMA = vol.Schema({}) -SERVICE_DISABLE_SCHEMA = vol.Schema({}) - DEFAULT_URL = "sqlite:///{hass_config_path}" DEFAULT_DB_FILE = "home-assistant_v2.db" DEFAULT_DB_INTEGRITY_CHECK = True @@ -196,7 +165,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: instance.async_initialize() instance.async_register() instance.start() - _async_register_services(hass, instance) + async_register_services(hass, instance) history.async_setup(hass) statistics.async_setup(hass) websocket_api.async_setup(hass) @@ -211,51 +180,3 @@ async def _process_recorder_platform( """Process a recorder platform.""" instance: Recorder = hass.data[DATA_INSTANCE] instance.queue.put(AddRecorderPlatformTask(domain, platform)) - - -@callback -def _async_register_services(hass: HomeAssistant, instance: Recorder) -> None: - """Register recorder services.""" - - async def async_handle_purge_service(service: ServiceCall) -> None: - """Handle calls to the purge service.""" - instance.do_adhoc_purge(**service.data) - - hass.services.async_register( - DOMAIN, SERVICE_PURGE, async_handle_purge_service, schema=SERVICE_PURGE_SCHEMA - ) - - async def async_handle_purge_entities_service(service: ServiceCall) -> None: - """Handle calls to the purge entities service.""" - entity_ids = await async_extract_entity_ids(hass, service) - domains = service.data.get(ATTR_DOMAINS, []) - entity_globs = service.data.get(ATTR_ENTITY_GLOBS, []) - - instance.do_adhoc_purge_entities(entity_ids, domains, entity_globs) - - hass.services.async_register( - DOMAIN, - SERVICE_PURGE_ENTITIES, - async_handle_purge_entities_service, - schema=SERVICE_PURGE_ENTITIES_SCHEMA, - ) - - async def async_handle_enable_service(service: ServiceCall) -> None: - instance.set_enable(True) - - hass.services.async_register( - DOMAIN, - SERVICE_ENABLE, - async_handle_enable_service, - schema=SERVICE_ENABLE_SCHEMA, - ) - - async def async_handle_disable_service(service: ServiceCall) -> None: - instance.set_enable(False) - - hass.services.async_register( - DOMAIN, - SERVICE_DISABLE, - async_handle_disable_service, - schema=SERVICE_DISABLE_SCHEMA, - ) diff --git a/homeassistant/components/recorder/services.py b/homeassistant/components/recorder/services.py new file mode 100644 index 00000000000..3da2c63a27c --- /dev/null +++ b/homeassistant/components/recorder/services.py @@ -0,0 +1,106 @@ +"""Support for recorder services.""" +from __future__ import annotations + +import voluptuous as vol + +from homeassistant.core import HomeAssistant, ServiceCall, callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.service import async_extract_entity_ids + +from .const import ATTR_APPLY_FILTER, ATTR_KEEP_DAYS, ATTR_REPACK, DOMAIN +from .core import Recorder + +SERVICE_PURGE = "purge" +SERVICE_PURGE_ENTITIES = "purge_entities" +SERVICE_ENABLE = "enable" +SERVICE_DISABLE = "disable" + + +SERVICE_PURGE_SCHEMA = vol.Schema( + { + vol.Optional(ATTR_KEEP_DAYS): cv.positive_int, + vol.Optional(ATTR_REPACK, default=False): cv.boolean, + vol.Optional(ATTR_APPLY_FILTER, default=False): cv.boolean, + } +) + +ATTR_DOMAINS = "domains" +ATTR_ENTITY_GLOBS = "entity_globs" + +SERVICE_PURGE_ENTITIES_SCHEMA = vol.Schema( + { + vol.Optional(ATTR_DOMAINS, default=[]): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_ENTITY_GLOBS, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + } +).extend(cv.ENTITY_SERVICE_FIELDS) + +SERVICE_ENABLE_SCHEMA = vol.Schema({}) +SERVICE_DISABLE_SCHEMA = vol.Schema({}) + + +@callback +def _async_register_purge_service(hass: HomeAssistant, instance: Recorder) -> None: + async def async_handle_purge_service(service: ServiceCall) -> None: + """Handle calls to the purge service.""" + instance.do_adhoc_purge(**service.data) + + hass.services.async_register( + DOMAIN, SERVICE_PURGE, async_handle_purge_service, schema=SERVICE_PURGE_SCHEMA + ) + + +@callback +def _async_register_purge_entities_service( + hass: HomeAssistant, instance: Recorder +) -> None: + async def async_handle_purge_entities_service(service: ServiceCall) -> None: + """Handle calls to the purge entities service.""" + entity_ids = await async_extract_entity_ids(hass, service) + domains = service.data.get(ATTR_DOMAINS, []) + entity_globs = service.data.get(ATTR_ENTITY_GLOBS, []) + + instance.do_adhoc_purge_entities(entity_ids, domains, entity_globs) + + hass.services.async_register( + DOMAIN, + SERVICE_PURGE_ENTITIES, + async_handle_purge_entities_service, + schema=SERVICE_PURGE_ENTITIES_SCHEMA, + ) + + +@callback +def _async_register_enable_service(hass: HomeAssistant, instance: Recorder) -> None: + async def async_handle_enable_service(service: ServiceCall) -> None: + instance.set_enable(True) + + hass.services.async_register( + DOMAIN, + SERVICE_ENABLE, + async_handle_enable_service, + schema=SERVICE_ENABLE_SCHEMA, + ) + + +@callback +def _async_register_disable_service(hass: HomeAssistant, instance: Recorder) -> None: + async def async_handle_disable_service(service: ServiceCall) -> None: + instance.set_enable(False) + + hass.services.async_register( + DOMAIN, + SERVICE_DISABLE, + async_handle_disable_service, + schema=SERVICE_DISABLE_SCHEMA, + ) + + +@callback +def async_register_services(hass: HomeAssistant, instance: Recorder) -> None: + """Register recorder services.""" + _async_register_purge_service(hass, instance) + _async_register_purge_entities_service(hass, instance) + _async_register_enable_service(hass, instance) + _async_register_disable_service(hass, instance) diff --git a/mypy.ini b/mypy.ini index b55d5e74998..3d97a716955 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1831,6 +1831,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.recorder.services] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.recorder.statistics] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index fc2c03e0039..1e8ff89f20d 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -20,10 +20,6 @@ from homeassistant.components.recorder import ( CONF_DB_URL, CONFIG_SCHEMA, DOMAIN, - SERVICE_DISABLE, - SERVICE_ENABLE, - SERVICE_PURGE, - SERVICE_PURGE_ENTITIES, SQLITE_URL_PREFIX, Recorder, get_instance, @@ -38,6 +34,12 @@ from homeassistant.components.recorder.models import ( StatisticsRuns, process_timestamp, ) +from homeassistant.components.recorder.services import ( + SERVICE_DISABLE, + SERVICE_ENABLE, + SERVICE_PURGE, + SERVICE_PURGE_ENTITIES, +) from homeassistant.components.recorder.util import session_scope from homeassistant.const import ( EVENT_HOMEASSISTANT_FINAL_WRITE, diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 8ac2f3a783c..7c3a2adcdc1 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -19,6 +19,10 @@ from homeassistant.components.recorder.models import ( StatisticsShortTerm, ) from homeassistant.components.recorder.purge import purge_old_data +from homeassistant.components.recorder.services import ( + SERVICE_PURGE, + SERVICE_PURGE_ENTITIES, +) from homeassistant.components.recorder.tasks import PurgeTask from homeassistant.components.recorder.util import session_scope from homeassistant.const import EVENT_STATE_CHANGED, STATE_ON @@ -133,9 +137,7 @@ async def test_purge_old_states_encouters_database_corruption( "homeassistant.components.recorder.purge.purge_old_data", side_effect=sqlite3_exception, ): - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, {"keep_days": 0} - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, {"keep_days": 0}) await hass.async_block_till_done() await async_wait_recording_done(hass) @@ -169,9 +171,7 @@ async def test_purge_old_states_encounters_temporary_mysql_error( ), patch.object( instance.engine.dialect, "name", "mysql" ): - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, {"keep_days": 0} - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, {"keep_days": 0}) await hass.async_block_till_done() await async_wait_recording_done(hass) await async_wait_recording_done(hass) @@ -197,9 +197,7 @@ async def test_purge_old_states_encounters_operational_error( "homeassistant.components.recorder.purge._purge_old_recorder_runs", side_effect=exception, ): - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, {"keep_days": 0} - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, {"keep_days": 0}) await hass.async_block_till_done() await async_wait_recording_done(hass) await async_wait_recording_done(hass) @@ -452,9 +450,7 @@ async def test_purge_edge_case( events = session.query(Events).filter(Events.event_type == "EVENT_TEST_PURGE") assert events.count() == 1 - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, service_data - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) await hass.async_block_till_done() await async_recorder_block_till_done(hass) @@ -722,9 +718,7 @@ async def test_purge_filtered_states( assert events_keep.count() == 1 # Normal purge doesn't remove excluded entities - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, service_data - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) await hass.async_block_till_done() await async_recorder_block_till_done(hass) @@ -742,9 +736,7 @@ async def test_purge_filtered_states( # Test with 'apply_filter' = True service_data["apply_filter"] = True - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, service_data - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) await hass.async_block_till_done() await async_recorder_block_till_done(hass) @@ -780,9 +772,7 @@ async def test_purge_filtered_states( assert session.query(StateAttributes).count() == 11 # Do it again to make sure nothing changes - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, service_data - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) @@ -794,9 +784,7 @@ async def test_purge_filtered_states( assert session.query(StateAttributes).count() == 11 service_data = {"keep_days": 0} - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, service_data - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) @@ -844,9 +832,7 @@ async def test_purge_filtered_states_to_empty( # Test with 'apply_filter' = True service_data["apply_filter"] = True - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, service_data - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) @@ -858,9 +844,7 @@ async def test_purge_filtered_states_to_empty( # Do it again to make sure nothing changes # Why do we do this? Should we check the end result? - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, service_data - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) @@ -914,9 +898,7 @@ async def test_purge_without_state_attributes_filtered_states_to_empty( # Test with 'apply_filter' = True service_data["apply_filter"] = True - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, service_data - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) @@ -928,9 +910,7 @@ async def test_purge_without_state_attributes_filtered_states_to_empty( # Do it again to make sure nothing changes # Why do we do this? Should we check the end result? - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, service_data - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) @@ -985,9 +965,7 @@ async def test_purge_filtered_events( assert states.count() == 10 # Normal purge doesn't remove excluded events - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, service_data - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) await hass.async_block_till_done() await async_recorder_block_till_done(hass) @@ -1005,9 +983,7 @@ async def test_purge_filtered_events( # Test with 'apply_filter' = True service_data["apply_filter"] = True - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, service_data - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) await hass.async_block_till_done() await async_recorder_block_till_done(hass) @@ -1105,9 +1081,7 @@ async def test_purge_filtered_events_state_changed( assert events_purge.count() == 60 assert states.count() == 63 - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, service_data - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) await hass.async_block_till_done() await async_recorder_block_till_done(hass) @@ -1146,7 +1120,7 @@ async def test_purge_entities( } await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE_ENTITIES, service_data + recorder.DOMAIN, SERVICE_PURGE_ENTITIES, service_data ) await hass.async_block_till_done()