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") if conf := config.get(DOMAIN, {}): filters = sqlalchemy_filter_from_include_exclude_conf(conf) entities_filter = convert_include_exclude_filter(conf)
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)
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 async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the history hooks.""" conf = config.get(DOMAIN, {}) hass.data[HISTORY_FILTERS] = filters = sqlalchemy_filter_from_include_exclude_conf( conf ) hass.data[HISTORY_USE_INCLUDE_ORDER] = use_include_order = conf.get(CONF_ORDER) hass.http.register_view(HistoryPeriodView(filters, use_include_order)) frontend.async_register_built_in_panel(hass, "history", "history", "hass:chart-box") websocket_api.async_register_command(hass, ws_get_statistics_during_period) websocket_api.async_register_command(hass, ws_get_list_statistic_ids) websocket_api.async_register_command(hass, ws_get_history_during_period) return True
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)