async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Logbook setup.""" hass.data[DOMAIN] = {} @callback def log_message(service: ServiceCall) -> None: """Handle sending notification message service calls.""" message = service.data[ATTR_MESSAGE] name = service.data[ATTR_NAME] domain = service.data.get(ATTR_DOMAIN) entity_id = service.data.get(ATTR_ENTITY_ID) if entity_id is None and domain is None: # If there is no entity_id or # domain, the event will get filtered # away so we use the "logbook" domain domain = DOMAIN message.hass = hass message = message.async_render(parse_result=False) async_log_entry(hass, name, message, domain, entity_id, service.context) frontend.async_register_built_in_panel(hass, "logbook", "logbook", "hass:format-list-bulleted-type") recorder_conf = config.get(RECORDER_DOMAIN, {}) logbook_conf = config.get(DOMAIN, {}) recorder_filter = extract_include_exclude_filter_conf(recorder_conf) logbook_filter = extract_include_exclude_filter_conf(logbook_conf) merged_filter = merge_include_exclude_filters(recorder_filter, logbook_filter) possible_merged_entities_filter = convert_include_exclude_filter( merged_filter) if not possible_merged_entities_filter.empty_filter: filters = sqlalchemy_filter_from_include_exclude_conf(merged_filter) entities_filter = possible_merged_entities_filter else: filters = None entities_filter = None hass.data[LOGBOOK_FILTERS] = filters hass.data[LOGBOOK_ENTITIES_FILTER] = entities_filter websocket_api.async_setup(hass) rest_api.async_setup(hass, config, filters, entities_filter) hass.services.async_register(DOMAIN, "log", log_message, schema=LOG_MESSAGE_SCHEMA) await async_process_integration_platforms(hass, DOMAIN, _process_logbook_platform) return True
async def test_included_and_excluded_simple_case_no_domains( hass, recorder_mock): """Test filters with included and excluded without domains.""" filter_accept = {"sensor.kitchen4", "switch.kitchen"} filter_reject = { "light.any", "switch.other", "cover.any", "sensor.weather5", "light.kitchen", } conf = { CONF_INCLUDE: { CONF_ENTITY_GLOBS: ["sensor.kitchen*"], CONF_ENTITIES: ["switch.kitchen"], }, CONF_EXCLUDE: { CONF_ENTITY_GLOBS: ["sensor.weather*"], CONF_ENTITIES: ["light.kitchen"], }, } extracted_filter = extract_include_exclude_filter_conf(conf) entity_filter = convert_include_exclude_filter(extracted_filter) sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf( extracted_filter) assert sqlalchemy_filter is not None for entity_id in filter_accept: assert entity_filter(entity_id) is True for entity_id in filter_reject: assert entity_filter(entity_id) is False assert not entity_filter.explicitly_included("light.any") assert not entity_filter.explicitly_included("switch.other") assert entity_filter.explicitly_included("sensor.kitchen4") assert entity_filter.explicitly_included("switch.kitchen") assert not entity_filter.explicitly_excluded("light.any") assert not entity_filter.explicitly_excluded("switch.other") assert entity_filter.explicitly_excluded("sensor.weather5") assert entity_filter.explicitly_excluded("light.kitchen") ( filtered_states_entity_ids, filtered_events_entity_ids, ) = await _async_get_states_and_events_with_filter( hass, sqlalchemy_filter, filter_accept | filter_reject) assert filtered_states_entity_ids == filter_accept assert not filtered_states_entity_ids.intersection(filter_reject) assert filtered_events_entity_ids == filter_accept assert not filtered_events_entity_ids.intersection(filter_reject)
async def test_included_and_excluded_complex_case(hass, recorder_mock): """Test filters with included and excluded with a complex filter.""" filter_accept = {"light.any", "sensor.kitchen_4", "switch.kitchen"} filter_reject = { "camera.one", "notify.any", "automation.update_readme", "automation.update_utilities_cost", "binary_sensor.iss", } conf = { CONF_INCLUDE: { CONF_ENTITIES: ["group.trackers"], }, CONF_EXCLUDE: { CONF_ENTITIES: [ "automation.update_readme", "automation.update_utilities_cost", "binary_sensor.iss", ], CONF_DOMAINS: [ "camera", "group", "media_player", "notify", "scene", "sun", "zone", ], }, } extracted_filter = extract_include_exclude_filter_conf(conf) entity_filter = convert_include_exclude_filter(extracted_filter) sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf( extracted_filter) assert sqlalchemy_filter is not None for entity_id in filter_accept: assert entity_filter(entity_id) is True for entity_id in filter_reject: assert entity_filter(entity_id) is False ( filtered_states_entity_ids, filtered_events_entity_ids, ) = await _async_get_states_and_events_with_filter( hass, sqlalchemy_filter, filter_accept | filter_reject) assert filtered_states_entity_ids == filter_accept assert not filtered_states_entity_ids.intersection(filter_reject) assert filtered_events_entity_ids == filter_accept assert not filtered_events_entity_ids.intersection(filter_reject)
def test_merge_include_exclude_filters(): """Test we can merge two filters together.""" include_exclude_filter_base = extract_include_exclude_filter_conf( SIMPLE_INCLUDE_EXCLUDE_FILTER ) include_filter_add = extract_include_exclude_filter_conf( SIMPLE_INCLUDE_FILTER_DIFFERENT_ENTITIES ) merged_filter = merge_include_exclude_filters( include_exclude_filter_base, include_filter_add ) assert merged_filter == { CONF_EXCLUDE: { CONF_DOMAINS: {"homeassistant"}, CONF_ENTITIES: {"sensor.one"}, CONF_ENTITY_GLOBS: {"climate.*"}, }, CONF_INCLUDE: { CONF_DOMAINS: {"other", "homeassistant"}, CONF_ENTITIES: {"not_sensor.one", "sensor.one"}, CONF_ENTITY_GLOBS: {"climate.*", "not_climate.*"}, }, }
async def test_same_entity_included_excluded_include_domain_wins( hass, recorder_mock): """Test filters with domain and entities and the include domain wins.""" filter_accept = { "media_player.test2", "media_player.test3", "thermostat.test", } filter_reject = { "thermostat.test2", "zone.home", "script.can_cancel_this_one", } conf = { CONF_INCLUDE: { CONF_DOMAINS: ["media_player"], CONF_ENTITIES: ["thermostat.test"], }, CONF_EXCLUDE: { CONF_DOMAINS: ["thermostat"], CONF_ENTITIES: ["media_player.test"], }, } extracted_filter = extract_include_exclude_filter_conf(conf) entity_filter = convert_include_exclude_filter(extracted_filter) sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf( extracted_filter) assert sqlalchemy_filter is not None for entity_id in filter_accept: assert entity_filter(entity_id) is True for entity_id in filter_reject: assert entity_filter(entity_id) is False ( filtered_states_entity_ids, filtered_events_entity_ids, ) = await _async_get_states_and_events_with_filter( hass, sqlalchemy_filter, filter_accept | filter_reject) assert filtered_states_entity_ids == filter_accept assert not filtered_states_entity_ids.intersection(filter_reject) assert filtered_events_entity_ids == filter_accept assert not filtered_events_entity_ids.intersection(filter_reject)
async def test_specificly_included_entity_always_wins(hass, recorder_mock): """Test specificlly included entity always wins.""" filter_accept = { "media_player.test2", "media_player.test3", "thermostat.test", "binary_sensor.specific_include", } filter_reject = { "binary_sensor.test2", "binary_sensor.home", "binary_sensor.can_cancel_this_one", } conf = { CONF_INCLUDE: { CONF_ENTITIES: ["binary_sensor.specific_include"], }, CONF_EXCLUDE: { CONF_DOMAINS: ["binary_sensor"], CONF_ENTITY_GLOBS: ["binary_sensor.*"], }, } extracted_filter = extract_include_exclude_filter_conf(conf) entity_filter = convert_include_exclude_filter(extracted_filter) sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf( extracted_filter) assert sqlalchemy_filter is not None for entity_id in filter_accept: assert entity_filter(entity_id) is True for entity_id in filter_reject: assert entity_filter(entity_id) is False ( filtered_states_entity_ids, filtered_events_entity_ids, ) = await _async_get_states_and_events_with_filter( hass, sqlalchemy_filter, filter_accept | filter_reject) assert filtered_states_entity_ids == filter_accept assert not filtered_states_entity_ids.intersection(filter_reject) assert filtered_events_entity_ids == filter_accept assert not filtered_events_entity_ids.intersection(filter_reject)
async def test_included_and_excluded_simple_case_no_globs(hass, recorder_mock): """Test filters with included and excluded without globs.""" filter_accept = {"switch.bla", "sensor.blu", "sensor.keep"} filter_reject = {"sensor.bli"} conf = { CONF_INCLUDE: { CONF_DOMAINS: ["sensor", "homeassistant"], CONF_ENTITIES: ["switch.bla"], }, CONF_EXCLUDE: { CONF_DOMAINS: ["switch"], CONF_ENTITIES: ["sensor.bli"], }, } extracted_filter = extract_include_exclude_filter_conf(conf) entity_filter = convert_include_exclude_filter(extracted_filter) sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf( extracted_filter) assert sqlalchemy_filter is not None for entity_id in filter_accept: assert entity_filter(entity_id) is True for entity_id in filter_reject: assert entity_filter(entity_id) is False ( filtered_states_entity_ids, filtered_events_entity_ids, ) = await _async_get_states_and_events_with_filter( hass, sqlalchemy_filter, filter_accept | filter_reject) assert filtered_states_entity_ids == filter_accept assert not filtered_states_entity_ids.intersection(filter_reject) assert filtered_events_entity_ids == filter_accept assert not filtered_events_entity_ids.intersection(filter_reject)
async def test_specificly_included_entity_always_wins_over_glob( hass, recorder_mock): """Test specificlly included entity always wins over a glob.""" filter_accept = { "sensor.apc900va_status", "sensor.apc900va_battery_charge", "sensor.apc900va_battery_runtime", "sensor.apc900va_load", "sensor.energy_x", } filter_reject = { "sensor.apc900va_not_included", } conf = { CONF_EXCLUDE: { CONF_DOMAINS: [ "updater", "camera", "group", "media_player", "script", "sun", "automation", "zone", "weblink", "scene", "calendar", "weather", "remote", "notify", "switch", "shell_command", "media_player", ], CONF_ENTITY_GLOBS: ["sensor.apc900va_*"], }, CONF_INCLUDE: { CONF_DOMAINS: [ "binary_sensor", "climate", "device_tracker", "input_boolean", "sensor", ], CONF_ENTITY_GLOBS: ["sensor.energy_*"], CONF_ENTITIES: [ "sensor.apc900va_status", "sensor.apc900va_battery_charge", "sensor.apc900va_battery_runtime", "sensor.apc900va_load", ], }, } extracted_filter = extract_include_exclude_filter_conf(conf) entity_filter = convert_include_exclude_filter(extracted_filter) sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf( extracted_filter) assert sqlalchemy_filter is not None for entity_id in filter_accept: assert entity_filter(entity_id) is True for entity_id in filter_reject: assert entity_filter(entity_id) is False ( filtered_states_entity_ids, filtered_events_entity_ids, ) = await _async_get_states_and_events_with_filter( hass, sqlalchemy_filter, filter_accept | filter_reject) assert filtered_states_entity_ids == filter_accept assert not filtered_states_entity_ids.intersection(filter_reject) assert filtered_events_entity_ids == filter_accept assert not filtered_events_entity_ids.intersection(filter_reject)
def test_extract_include_exclude_filter_conf(): """Test we can extract a filter from configuration without altering it.""" include_filter = extract_include_exclude_filter_conf(SIMPLE_INCLUDE_FILTER) assert include_filter == { CONF_EXCLUDE: { CONF_DOMAINS: set(), CONF_ENTITIES: set(), CONF_ENTITY_GLOBS: set(), }, CONF_INCLUDE: { CONF_DOMAINS: {"homeassistant"}, CONF_ENTITIES: {"sensor.one"}, CONF_ENTITY_GLOBS: {"climate.*"}, }, } exclude_filter = extract_include_exclude_filter_conf(SIMPLE_EXCLUDE_FILTER) assert exclude_filter == { CONF_INCLUDE: { CONF_DOMAINS: set(), CONF_ENTITIES: set(), CONF_ENTITY_GLOBS: set(), }, CONF_EXCLUDE: { CONF_DOMAINS: {"homeassistant"}, CONF_ENTITIES: {"sensor.one"}, CONF_ENTITY_GLOBS: {"climate.*"}, }, } include_exclude_filter = extract_include_exclude_filter_conf( SIMPLE_INCLUDE_EXCLUDE_FILTER ) assert include_exclude_filter == { CONF_INCLUDE: { CONF_DOMAINS: {"homeassistant"}, CONF_ENTITIES: {"sensor.one"}, CONF_ENTITY_GLOBS: {"climate.*"}, }, CONF_EXCLUDE: { CONF_DOMAINS: {"homeassistant"}, CONF_ENTITIES: {"sensor.one"}, CONF_ENTITY_GLOBS: {"climate.*"}, }, } include_exclude_filter[CONF_EXCLUDE][CONF_ENTITIES] = {"cover.altered"} # verify it really is a copy assert SIMPLE_INCLUDE_EXCLUDE_FILTER[CONF_EXCLUDE][CONF_ENTITIES] != { "cover.altered" } empty_include_filter = extract_include_exclude_filter_conf(EMPTY_INCLUDE_FILTER) assert empty_include_filter == { CONF_EXCLUDE: { CONF_DOMAINS: set(), CONF_ENTITIES: set(), CONF_ENTITY_GLOBS: set(), }, CONF_INCLUDE: { CONF_DOMAINS: set(), CONF_ENTITIES: set(), CONF_ENTITY_GLOBS: set(), }, }