Don't allow in-memory SQLite database (#69616)
This commit is contained in:
parent
fab1f29a29
commit
949b0e1b65
8 changed files with 77 additions and 40 deletions
|
@ -177,6 +177,19 @@ FILTER_SCHEMA = INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA.extend(
|
||||||
{vol.Optional(CONF_EXCLUDE, default=EXCLUDE_SCHEMA({})): EXCLUDE_SCHEMA}
|
{vol.Optional(CONF_EXCLUDE, default=EXCLUDE_SCHEMA({})): EXCLUDE_SCHEMA}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
ALLOW_IN_MEMORY_DB = False
|
||||||
|
|
||||||
|
|
||||||
|
def validate_db_url(db_url: str) -> Any:
|
||||||
|
"""Validate database URL."""
|
||||||
|
# Don't allow on-memory sqlite databases
|
||||||
|
if (db_url == SQLITE_URL_PREFIX or ":memory:" in db_url) and not ALLOW_IN_MEMORY_DB:
|
||||||
|
raise vol.Invalid("In-memory SQLite database is not supported")
|
||||||
|
|
||||||
|
return db_url
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema(
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Optional(DOMAIN, default=dict): vol.All(
|
vol.Optional(DOMAIN, default=dict): vol.All(
|
||||||
|
@ -190,7 +203,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||||
vol.Coerce(int), vol.Range(min=1)
|
vol.Coerce(int), vol.Range(min=1)
|
||||||
),
|
),
|
||||||
vol.Optional(CONF_PURGE_INTERVAL, default=1): cv.positive_int,
|
vol.Optional(CONF_PURGE_INTERVAL, default=1): cv.positive_int,
|
||||||
vol.Optional(CONF_DB_URL): cv.string,
|
vol.Optional(CONF_DB_URL): vol.All(cv.string, validate_db_url),
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_COMMIT_INTERVAL, default=DEFAULT_COMMIT_INTERVAL
|
CONF_COMMIT_INTERVAL, default=DEFAULT_COMMIT_INTERVAL
|
||||||
): cv.positive_int,
|
): cv.positive_int,
|
||||||
|
|
|
@ -910,10 +910,16 @@ def init_recorder_component(hass, add_config=None):
|
||||||
if recorder.CONF_COMMIT_INTERVAL not in config:
|
if recorder.CONF_COMMIT_INTERVAL not in config:
|
||||||
config[recorder.CONF_COMMIT_INTERVAL] = 0
|
config[recorder.CONF_COMMIT_INTERVAL] = 0
|
||||||
|
|
||||||
with patch("homeassistant.components.recorder.migration.migrate_schema"):
|
with patch(
|
||||||
|
"homeassistant.components.recorder.ALLOW_IN_MEMORY_DB",
|
||||||
|
True,
|
||||||
|
), patch("homeassistant.components.recorder.migration.migrate_schema"):
|
||||||
assert setup_component(hass, recorder.DOMAIN, {recorder.DOMAIN: config})
|
assert setup_component(hass, recorder.DOMAIN, {recorder.DOMAIN: config})
|
||||||
assert recorder.DOMAIN in hass.config.components
|
assert recorder.DOMAIN in hass.config.components
|
||||||
_LOGGER.info("In-memory recorder successfully started")
|
_LOGGER.info(
|
||||||
|
"Test recorder successfully started, database location: %s",
|
||||||
|
config[recorder.CONF_DB_URL],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_init_recorder_component(hass, add_config=None):
|
async def async_init_recorder_component(hass, add_config=None):
|
||||||
|
@ -924,12 +930,18 @@ async def async_init_recorder_component(hass, add_config=None):
|
||||||
if recorder.CONF_COMMIT_INTERVAL not in config:
|
if recorder.CONF_COMMIT_INTERVAL not in config:
|
||||||
config[recorder.CONF_COMMIT_INTERVAL] = 0
|
config[recorder.CONF_COMMIT_INTERVAL] = 0
|
||||||
|
|
||||||
with patch("homeassistant.components.recorder.migration.migrate_schema"):
|
with patch(
|
||||||
|
"homeassistant.components.recorder.ALLOW_IN_MEMORY_DB",
|
||||||
|
True,
|
||||||
|
), patch("homeassistant.components.recorder.migration.migrate_schema"):
|
||||||
assert await async_setup_component(
|
assert await async_setup_component(
|
||||||
hass, recorder.DOMAIN, {recorder.DOMAIN: config}
|
hass, recorder.DOMAIN, {recorder.DOMAIN: config}
|
||||||
)
|
)
|
||||||
assert recorder.DOMAIN in hass.config.components
|
assert recorder.DOMAIN in hass.config.components
|
||||||
_LOGGER.info("In-memory recorder successfully started")
|
_LOGGER.info(
|
||||||
|
"Test recorder successfully started, database location: %s",
|
||||||
|
config[recorder.CONF_DB_URL],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def mock_restore_cache(hass, states):
|
def mock_restore_cache(hass, states):
|
||||||
|
|
|
@ -451,15 +451,13 @@ async def test_send_with_no_energy(hass, aioclient_mock):
|
||||||
assert "energy" not in postdata
|
assert "energy" not in postdata
|
||||||
|
|
||||||
|
|
||||||
async def test_send_with_no_energy_config(hass, aioclient_mock):
|
async def test_send_with_no_energy_config(hass, aioclient_mock, recorder_mock):
|
||||||
"""Test send base preferences are defined."""
|
"""Test send base preferences are defined."""
|
||||||
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
|
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
|
||||||
analytics = Analytics(hass)
|
analytics = Analytics(hass)
|
||||||
|
|
||||||
await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True})
|
await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True})
|
||||||
assert await async_setup_component(
|
assert await async_setup_component(hass, "energy", {})
|
||||||
hass, "energy", {"recorder": {"db_url": "sqlite://"}}
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex, patch(
|
with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex, patch(
|
||||||
"homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION
|
"homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION
|
||||||
|
@ -475,15 +473,13 @@ async def test_send_with_no_energy_config(hass, aioclient_mock):
|
||||||
assert not postdata["energy"]["configured"]
|
assert not postdata["energy"]["configured"]
|
||||||
|
|
||||||
|
|
||||||
async def test_send_with_energy_config(hass, aioclient_mock):
|
async def test_send_with_energy_config(hass, aioclient_mock, recorder_mock):
|
||||||
"""Test send base preferences are defined."""
|
"""Test send base preferences are defined."""
|
||||||
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
|
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
|
||||||
analytics = Analytics(hass)
|
analytics = Analytics(hass)
|
||||||
|
|
||||||
await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True})
|
await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True})
|
||||||
assert await async_setup_component(
|
assert await async_setup_component(hass, "energy", {})
|
||||||
hass, "energy", {"recorder": {"db_url": "sqlite://"}}
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex, patch(
|
with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex, patch(
|
||||||
"homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION
|
"homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION
|
||||||
|
|
|
@ -29,12 +29,15 @@ from tests.common import async_init_recorder_component
|
||||||
from tests.components.recorder.common import async_wait_recording_done_without_instance
|
from tests.components.recorder.common import async_wait_recording_done_without_instance
|
||||||
|
|
||||||
|
|
||||||
async def setup_integration(hass):
|
@pytest.fixture
|
||||||
|
async def setup_integration(recorder_mock):
|
||||||
"""Set up the integration."""
|
"""Set up the integration."""
|
||||||
assert await async_setup_component(
|
|
||||||
hass, "energy", {"recorder": {"db_url": "sqlite://"}}
|
async def setup_integration(hass):
|
||||||
)
|
assert await async_setup_component(hass, "energy", {})
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
return setup_integration
|
||||||
|
|
||||||
|
|
||||||
def get_statistics_for_entity(statistics_results, entity_id):
|
def get_statistics_for_entity(statistics_results, entity_id):
|
||||||
|
@ -45,7 +48,7 @@ def get_statistics_for_entity(statistics_results, entity_id):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def test_cost_sensor_no_states(hass, hass_storage) -> None:
|
async def test_cost_sensor_no_states(hass, hass_storage, setup_integration) -> None:
|
||||||
"""Test sensors are created."""
|
"""Test sensors are created."""
|
||||||
energy_data = data.EnergyManager.default_preferences()
|
energy_data = data.EnergyManager.default_preferences()
|
||||||
energy_data["energy_sources"].append(
|
energy_data["energy_sources"].append(
|
||||||
|
@ -91,6 +94,7 @@ async def test_cost_sensor_price_entity_total_increasing(
|
||||||
hass,
|
hass,
|
||||||
hass_storage,
|
hass_storage,
|
||||||
hass_ws_client,
|
hass_ws_client,
|
||||||
|
setup_integration,
|
||||||
initial_energy,
|
initial_energy,
|
||||||
initial_cost,
|
initial_cost,
|
||||||
price_entity,
|
price_entity,
|
||||||
|
@ -294,6 +298,7 @@ async def test_cost_sensor_price_entity_total(
|
||||||
hass,
|
hass,
|
||||||
hass_storage,
|
hass_storage,
|
||||||
hass_ws_client,
|
hass_ws_client,
|
||||||
|
setup_integration,
|
||||||
initial_energy,
|
initial_energy,
|
||||||
initial_cost,
|
initial_cost,
|
||||||
price_entity,
|
price_entity,
|
||||||
|
@ -500,6 +505,7 @@ async def test_cost_sensor_price_entity_total_no_reset(
|
||||||
hass,
|
hass,
|
||||||
hass_storage,
|
hass_storage,
|
||||||
hass_ws_client,
|
hass_ws_client,
|
||||||
|
setup_integration,
|
||||||
initial_energy,
|
initial_energy,
|
||||||
initial_cost,
|
initial_cost,
|
||||||
price_entity,
|
price_entity,
|
||||||
|
@ -670,7 +676,7 @@ async def test_cost_sensor_price_entity_total_no_reset(
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_cost_sensor_handle_energy_units(
|
async def test_cost_sensor_handle_energy_units(
|
||||||
hass, hass_storage, energy_unit, factor
|
hass, hass_storage, setup_integration, energy_unit, factor
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test energy cost price from sensor entity."""
|
"""Test energy cost price from sensor entity."""
|
||||||
energy_attributes = {
|
energy_attributes = {
|
||||||
|
@ -736,7 +742,7 @@ async def test_cost_sensor_handle_energy_units(
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_cost_sensor_handle_price_units(
|
async def test_cost_sensor_handle_price_units(
|
||||||
hass, hass_storage, price_unit, factor
|
hass, hass_storage, setup_integration, price_unit, factor
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test energy cost price from sensor entity."""
|
"""Test energy cost price from sensor entity."""
|
||||||
energy_attributes = {
|
energy_attributes = {
|
||||||
|
@ -798,7 +804,7 @@ async def test_cost_sensor_handle_price_units(
|
||||||
assert state.state == "20.0"
|
assert state.state == "20.0"
|
||||||
|
|
||||||
|
|
||||||
async def test_cost_sensor_handle_gas(hass, hass_storage) -> None:
|
async def test_cost_sensor_handle_gas(hass, hass_storage, setup_integration) -> None:
|
||||||
"""Test gas cost price from sensor entity."""
|
"""Test gas cost price from sensor entity."""
|
||||||
energy_attributes = {
|
energy_attributes = {
|
||||||
ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS,
|
ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS,
|
||||||
|
@ -847,7 +853,9 @@ async def test_cost_sensor_handle_gas(hass, hass_storage) -> None:
|
||||||
assert state.state == "50.0"
|
assert state.state == "50.0"
|
||||||
|
|
||||||
|
|
||||||
async def test_cost_sensor_handle_gas_kwh(hass, hass_storage) -> None:
|
async def test_cost_sensor_handle_gas_kwh(
|
||||||
|
hass, hass_storage, setup_integration
|
||||||
|
) -> None:
|
||||||
"""Test gas cost price from sensor entity."""
|
"""Test gas cost price from sensor entity."""
|
||||||
energy_attributes = {
|
energy_attributes = {
|
||||||
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
|
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
|
||||||
|
@ -898,7 +906,7 @@ async def test_cost_sensor_handle_gas_kwh(hass, hass_storage) -> None:
|
||||||
|
|
||||||
@pytest.mark.parametrize("state_class", [None])
|
@pytest.mark.parametrize("state_class", [None])
|
||||||
async def test_cost_sensor_wrong_state_class(
|
async def test_cost_sensor_wrong_state_class(
|
||||||
hass, hass_storage, caplog, state_class
|
hass, hass_storage, setup_integration, caplog, state_class
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test energy sensor rejects sensor with wrong state_class."""
|
"""Test energy sensor rejects sensor with wrong state_class."""
|
||||||
energy_attributes = {
|
energy_attributes = {
|
||||||
|
@ -960,7 +968,7 @@ async def test_cost_sensor_wrong_state_class(
|
||||||
|
|
||||||
@pytest.mark.parametrize("state_class", [SensorStateClass.MEASUREMENT])
|
@pytest.mark.parametrize("state_class", [SensorStateClass.MEASUREMENT])
|
||||||
async def test_cost_sensor_state_class_measurement_no_reset(
|
async def test_cost_sensor_state_class_measurement_no_reset(
|
||||||
hass, hass_storage, caplog, state_class
|
hass, hass_storage, setup_integration, caplog, state_class
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test energy sensor rejects state_class measurement with no last_reset."""
|
"""Test energy sensor rejects state_class measurement with no last_reset."""
|
||||||
energy_attributes = {
|
energy_attributes = {
|
||||||
|
|
|
@ -18,11 +18,9 @@ from tests.components.recorder.common import async_wait_recording_done_without_i
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
async def setup_integration(hass):
|
async def setup_integration(hass, recorder_mock):
|
||||||
"""Set up the integration."""
|
"""Set up the integration."""
|
||||||
assert await async_setup_component(
|
assert await async_setup_component(hass, "energy", {})
|
||||||
hass, "energy", {"recorder": {"db_url": "sqlite://"}}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
|
@ -1394,3 +1394,11 @@ async def test_database_lock_without_instance(hass):
|
||||||
assert await instance.lock_database()
|
assert await instance.lock_database()
|
||||||
finally:
|
finally:
|
||||||
assert instance.unlock_database()
|
assert instance.unlock_database()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_in_memory_database(hass, caplog):
|
||||||
|
"""Test connecting to an in-memory recorder is not allowed."""
|
||||||
|
assert not await async_setup_component(
|
||||||
|
hass, recorder.DOMAIN, {recorder.DOMAIN: {recorder.CONF_DB_URL: "sqlite://"}}
|
||||||
|
)
|
||||||
|
assert "In-memory SQLite database is not supported" in caplog.text
|
||||||
|
|
|
@ -43,7 +43,7 @@ async def test_schema_update_calls(hass):
|
||||||
"""Test that schema migrations occur in correct order."""
|
"""Test that schema migrations occur in correct order."""
|
||||||
assert recorder.util.async_migration_in_progress(hass) is False
|
assert recorder.util.async_migration_in_progress(hass) is False
|
||||||
|
|
||||||
with patch(
|
with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch(
|
||||||
"homeassistant.components.recorder.create_engine", new=create_engine_test
|
"homeassistant.components.recorder.create_engine", new=create_engine_test
|
||||||
), patch(
|
), patch(
|
||||||
"homeassistant.components.recorder.migration._apply_update",
|
"homeassistant.components.recorder.migration._apply_update",
|
||||||
|
@ -68,8 +68,9 @@ async def test_migration_in_progress(hass):
|
||||||
assert recorder.util.async_migration_in_progress(hass) is False
|
assert recorder.util.async_migration_in_progress(hass) is False
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.recorder.create_engine", new=create_engine_test
|
"homeassistant.components.recorder.ALLOW_IN_MEMORY_DB",
|
||||||
):
|
True,
|
||||||
|
), patch("homeassistant.components.recorder.create_engine", new=create_engine_test):
|
||||||
await async_setup_component(
|
await async_setup_component(
|
||||||
hass, "recorder", {"recorder": {"db_url": "sqlite://"}}
|
hass, "recorder", {"recorder": {"db_url": "sqlite://"}}
|
||||||
)
|
)
|
||||||
|
@ -84,7 +85,7 @@ async def test_database_migration_failed(hass):
|
||||||
"""Test we notify if the migration fails."""
|
"""Test we notify if the migration fails."""
|
||||||
assert recorder.util.async_migration_in_progress(hass) is False
|
assert recorder.util.async_migration_in_progress(hass) is False
|
||||||
|
|
||||||
with patch(
|
with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch(
|
||||||
"homeassistant.components.recorder.create_engine", new=create_engine_test
|
"homeassistant.components.recorder.create_engine", new=create_engine_test
|
||||||
), patch(
|
), patch(
|
||||||
"homeassistant.components.recorder.migration._apply_update",
|
"homeassistant.components.recorder.migration._apply_update",
|
||||||
|
@ -117,7 +118,7 @@ async def test_database_migration_encounters_corruption(hass):
|
||||||
sqlite3_exception = DatabaseError("statement", {}, [])
|
sqlite3_exception = DatabaseError("statement", {}, [])
|
||||||
sqlite3_exception.__cause__ = sqlite3.DatabaseError()
|
sqlite3_exception.__cause__ = sqlite3.DatabaseError()
|
||||||
|
|
||||||
with patch(
|
with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch(
|
||||||
"homeassistant.components.recorder.migration.schema_is_current",
|
"homeassistant.components.recorder.migration.schema_is_current",
|
||||||
side_effect=[False, True],
|
side_effect=[False, True],
|
||||||
), patch(
|
), patch(
|
||||||
|
@ -141,7 +142,7 @@ async def test_database_migration_encounters_corruption_not_sqlite(hass):
|
||||||
"""Test we fail on database error when we cannot recover."""
|
"""Test we fail on database error when we cannot recover."""
|
||||||
assert recorder.util.async_migration_in_progress(hass) is False
|
assert recorder.util.async_migration_in_progress(hass) is False
|
||||||
|
|
||||||
with patch(
|
with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch(
|
||||||
"homeassistant.components.recorder.migration.schema_is_current",
|
"homeassistant.components.recorder.migration.schema_is_current",
|
||||||
side_effect=[False, True],
|
side_effect=[False, True],
|
||||||
), patch(
|
), patch(
|
||||||
|
@ -176,8 +177,9 @@ async def test_events_during_migration_are_queued(hass):
|
||||||
assert recorder.util.async_migration_in_progress(hass) is False
|
assert recorder.util.async_migration_in_progress(hass) is False
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.recorder.create_engine", new=create_engine_test
|
"homeassistant.components.recorder.ALLOW_IN_MEMORY_DB",
|
||||||
):
|
True,
|
||||||
|
), patch("homeassistant.components.recorder.create_engine", new=create_engine_test):
|
||||||
await async_setup_component(
|
await async_setup_component(
|
||||||
hass, "recorder", {"recorder": {"db_url": "sqlite://"}}
|
hass, "recorder", {"recorder": {"db_url": "sqlite://"}}
|
||||||
)
|
)
|
||||||
|
@ -200,7 +202,7 @@ async def test_events_during_migration_queue_exhausted(hass):
|
||||||
|
|
||||||
assert recorder.util.async_migration_in_progress(hass) is False
|
assert recorder.util.async_migration_in_progress(hass) is False
|
||||||
|
|
||||||
with patch(
|
with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch(
|
||||||
"homeassistant.components.recorder.create_engine", new=create_engine_test
|
"homeassistant.components.recorder.create_engine", new=create_engine_test
|
||||||
), patch.object(recorder, "MAX_QUEUE_BACKLOG", 1):
|
), patch.object(recorder, "MAX_QUEUE_BACKLOG", 1):
|
||||||
await async_setup_component(
|
await async_setup_component(
|
||||||
|
@ -283,7 +285,7 @@ async def test_schema_migrate(hass, start_version):
|
||||||
migration_version = res.schema_version
|
migration_version = res.schema_version
|
||||||
migration_done.set()
|
migration_done.set()
|
||||||
|
|
||||||
with patch(
|
with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch(
|
||||||
"homeassistant.components.recorder.create_engine", new=_create_engine_test
|
"homeassistant.components.recorder.create_engine", new=_create_engine_test
|
||||||
), patch(
|
), patch(
|
||||||
"homeassistant.components.recorder.Recorder._setup_run",
|
"homeassistant.components.recorder.Recorder._setup_run",
|
||||||
|
|
|
@ -332,7 +332,7 @@ async def test_recorder_info_migration_queue_exhausted(hass, hass_ws_client):
|
||||||
migration_done.wait()
|
migration_done.wait()
|
||||||
return real_migration(*args)
|
return real_migration(*args)
|
||||||
|
|
||||||
with patch(
|
with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch(
|
||||||
"homeassistant.components.recorder.Recorder.async_periodic_statistics"
|
"homeassistant.components.recorder.Recorder.async_periodic_statistics"
|
||||||
), patch(
|
), patch(
|
||||||
"homeassistant.components.recorder.create_engine", new=create_engine_test
|
"homeassistant.components.recorder.create_engine", new=create_engine_test
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue