Add and remove entities during runtime in Husqvarna Automower (#127878)

This commit is contained in:
Thomas55555 2024-10-29 12:46:04 +01:00 committed by GitHub
parent 2236ca3e12
commit 983cd9c3fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 275 additions and 146 deletions

View File

@ -13,6 +13,7 @@ from homeassistant.helpers import (
aiohttp_client, aiohttp_client,
config_entry_oauth2_flow, config_entry_oauth2_flow,
device_registry as dr, device_registry as dr,
entity_registry as er,
) )
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
@ -99,3 +100,20 @@ def cleanup_removed_devices(
device_reg.async_update_device( device_reg.async_update_device(
device.id, remove_config_entry_id=config_entry.entry_id device.id, remove_config_entry_id=config_entry.entry_id
) )
def remove_work_area_entities(
hass: HomeAssistant,
config_entry: ConfigEntry,
removed_work_areas: set[int],
mower_id: str,
) -> None:
"""Remove all unused work area entities for the specified mower."""
entity_reg = er.async_get(hass)
for entity_entry in er.async_entries_for_config_entry(
entity_reg, config_entry.entry_id
):
for work_area_id in removed_work_areas:
if entity_entry.unique_id.startswith(f"{mower_id}_{work_area_id}_"):
_LOGGER.info("Deleting: %s", entity_entry.entity_id)
entity_reg.async_remove(entity_entry.entity_id)

View File

@ -9,13 +9,12 @@ from typing import TYPE_CHECKING, Any
from aioautomower.exceptions import ApiException from aioautomower.exceptions import ApiException
from aioautomower.model import MowerActivities, MowerAttributes, MowerStates, WorkArea from aioautomower.model import MowerActivities, MowerAttributes, MowerStates, WorkArea
from homeassistant.core import HomeAssistant, callback from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AutomowerConfigEntry, AutomowerDataUpdateCoordinator from . import AutomowerDataUpdateCoordinator
from .const import DOMAIN, EXECUTION_TIME_DELAY from .const import DOMAIN, EXECUTION_TIME_DELAY
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -53,30 +52,6 @@ def _work_area_translation_key(work_area_id: int, key: str) -> str:
return f"work_area_{key}" return f"work_area_{key}"
@callback
def async_remove_work_area_entities(
hass: HomeAssistant,
coordinator: AutomowerDataUpdateCoordinator,
entry: AutomowerConfigEntry,
mower_id: str,
) -> None:
"""Remove deleted work areas from Home Assistant."""
entity_reg = er.async_get(hass)
active_work_areas = set()
_work_areas = coordinator.data[mower_id].work_areas
if _work_areas is not None:
for work_area_id in _work_areas:
uid = f"{mower_id}_{work_area_id}_cutting_height_work_area"
active_work_areas.add(uid)
for entity_entry in er.async_entries_for_config_entry(entity_reg, entry.entry_id):
if (
(split := entity_entry.unique_id.split("_"))[0] == mower_id
and split[-1] == "area"
and entity_entry.unique_id not in active_work_areas
):
entity_reg.async_remove(entity_entry.entity_id)
def handle_sending_exception( def handle_sending_exception(
poll_after_sending: bool = False, poll_after_sending: bool = False,
) -> Callable[ ) -> Callable[

View File

@ -13,13 +13,12 @@ from homeassistant.const import PERCENTAGE, EntityCategory
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AutomowerConfigEntry from . import AutomowerConfigEntry, remove_work_area_entities
from .coordinator import AutomowerDataUpdateCoordinator from .coordinator import AutomowerDataUpdateCoordinator
from .entity import ( from .entity import (
AutomowerControlEntity, AutomowerControlEntity,
WorkAreaControlEntity, WorkAreaControlEntity,
_work_area_translation_key, _work_area_translation_key,
async_remove_work_area_entities,
handle_sending_exception, handle_sending_exception,
) )
@ -110,26 +109,44 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up number platform.""" """Set up number platform."""
coordinator = entry.runtime_data coordinator = entry.runtime_data
entities: list[NumberEntity] = [] current_work_areas: dict[str, set[int]] = {}
async_add_entities(
AutomowerNumberEntity(mower_id, coordinator, description)
for mower_id in coordinator.data
for description in MOWER_NUMBER_TYPES
if description.exists_fn(coordinator.data[mower_id])
)
def _async_work_area_listener() -> None:
"""Listen for new work areas and add/remove entities as needed."""
for mower_id in coordinator.data: for mower_id in coordinator.data:
if coordinator.data[mower_id].capabilities.work_areas: if (
_work_areas = coordinator.data[mower_id].work_areas coordinator.data[mower_id].capabilities.work_areas
if _work_areas is not None: and (_work_areas := coordinator.data[mower_id].work_areas) is not None
entities.extend( ):
received_work_areas = set(_work_areas.keys())
current_work_area_set = current_work_areas.setdefault(mower_id, set())
new_work_areas = received_work_areas - current_work_area_set
removed_work_areas = current_work_area_set - received_work_areas
if new_work_areas:
current_work_area_set.update(new_work_areas)
async_add_entities(
WorkAreaNumberEntity( WorkAreaNumberEntity(
mower_id, coordinator, description, work_area_id mower_id, coordinator, description, work_area_id
) )
for description in WORK_AREA_NUMBER_TYPES for description in WORK_AREA_NUMBER_TYPES
for work_area_id in _work_areas for work_area_id in new_work_areas
) )
async_remove_work_area_entities(hass, coordinator, entry, mower_id)
entities.extend( if removed_work_areas:
AutomowerNumberEntity(mower_id, coordinator, description) remove_work_area_entities(hass, entry, removed_work_areas, mower_id)
for description in MOWER_NUMBER_TYPES current_work_area_set.difference_update(removed_work_areas)
if description.exists_fn(coordinator.data[mower_id])
) coordinator.async_add_listener(_async_work_area_listener)
async_add_entities(entities) _async_work_area_listener()
class AutomowerNumberEntity(AutomowerControlEntity, NumberEntity): class AutomowerNumberEntity(AutomowerControlEntity, NumberEntity):

View File

@ -431,25 +431,44 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up sensor platform.""" """Set up sensor platform."""
coordinator = entry.runtime_data coordinator = entry.runtime_data
entities: list[SensorEntity] = [] current_work_areas: dict[str, set[int]] = {}
async_add_entities(
AutomowerSensorEntity(mower_id, coordinator, description)
for mower_id, data in coordinator.data.items()
for description in MOWER_SENSOR_TYPES
if description.exists_fn(data)
)
def _async_work_area_listener() -> None:
"""Listen for new work areas and add sensor entities if they did not exist.
Listening for deletable work areas is managed in the number platform.
"""
for mower_id in coordinator.data: for mower_id in coordinator.data:
if coordinator.data[mower_id].capabilities.work_areas: if (
_work_areas = coordinator.data[mower_id].work_areas coordinator.data[mower_id].capabilities.work_areas
if _work_areas is not None: and (_work_areas := coordinator.data[mower_id].work_areas) is not None
entities.extend( ):
received_work_areas = set(_work_areas.keys())
new_work_areas = received_work_areas - current_work_areas.get(
mower_id, set()
)
if new_work_areas:
current_work_areas.setdefault(mower_id, set()).update(
new_work_areas
)
async_add_entities(
WorkAreaSensorEntity( WorkAreaSensorEntity(
mower_id, coordinator, description, work_area_id mower_id, coordinator, description, work_area_id
) )
for description in WORK_AREA_SENSOR_TYPES for description in WORK_AREA_SENSOR_TYPES
for work_area_id in _work_areas for work_area_id in new_work_areas
if description.exists_fn(_work_areas[work_area_id]) if description.exists_fn(_work_areas[work_area_id])
) )
entities.extend(
AutomowerSensorEntity(mower_id, coordinator, description) coordinator.async_add_listener(_async_work_area_listener)
for description in MOWER_SENSOR_TYPES _async_work_area_listener()
if description.exists_fn(coordinator.data[mower_id])
)
async_add_entities(entities)
class AutomowerSensorEntity(AutomowerBaseEntity, SensorEntity): class AutomowerSensorEntity(AutomowerBaseEntity, SensorEntity):

View File

@ -6,8 +6,7 @@ from typing import TYPE_CHECKING, Any
from aioautomower.model import MowerModes, StayOutZones, Zone from aioautomower.model import MowerModes, StayOutZones, Zone
from homeassistant.components.switch import SwitchEntity from homeassistant.components.switch import SwitchEntity
from homeassistant.const import Platform from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -30,28 +29,82 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up switch platform.""" """Set up switch platform."""
coordinator = entry.runtime_data coordinator = entry.runtime_data
entities: list[SwitchEntity] = [] current_work_areas: dict[str, set[int]] = {}
entities.extend( current_stay_out_zones: dict[str, set[str]] = {}
async_add_entities(
AutomowerScheduleSwitchEntity(mower_id, coordinator) AutomowerScheduleSwitchEntity(mower_id, coordinator)
for mower_id in coordinator.data for mower_id in coordinator.data
) )
def _async_work_area_listener() -> None:
"""Listen for new work areas and add switch entities if they did not exist.
Listening for deletable work areas is managed in the number platform.
"""
for mower_id in coordinator.data: for mower_id in coordinator.data:
if coordinator.data[mower_id].capabilities.stay_out_zones: if (
_stay_out_zones = coordinator.data[mower_id].stay_out_zones coordinator.data[mower_id].capabilities.work_areas
if _stay_out_zones is not None: and (_work_areas := coordinator.data[mower_id].work_areas) is not None
entities.extend( ):
StayOutZoneSwitchEntity(coordinator, mower_id, stay_out_zone_uid) received_work_areas = set(_work_areas.keys())
for stay_out_zone_uid in _stay_out_zones.zones new_work_areas = received_work_areas - current_work_areas.get(
mower_id, set()
) )
async_remove_entities(hass, coordinator, entry, mower_id) if new_work_areas:
if coordinator.data[mower_id].capabilities.work_areas: current_work_areas.setdefault(mower_id, set()).update(
_work_areas = coordinator.data[mower_id].work_areas new_work_areas
if _work_areas is not None: )
entities.extend( async_add_entities(
WorkAreaSwitchEntity(coordinator, mower_id, work_area_id) WorkAreaSwitchEntity(coordinator, mower_id, work_area_id)
for work_area_id in _work_areas for work_area_id in new_work_areas
) )
async_add_entities(entities)
def _remove_stay_out_zone_entities(
removed_stay_out_zones: set, mower_id: str
) -> None:
"""Remove all unused stay-out zones for all platforms."""
entity_reg = er.async_get(hass)
for entity_entry in er.async_entries_for_config_entry(
entity_reg, entry.entry_id
):
for stay_out_zone_uid in removed_stay_out_zones:
if entity_entry.unique_id.startswith(f"{mower_id}_{stay_out_zone_uid}"):
entity_reg.async_remove(entity_entry.entity_id)
def _async_stay_out_zone_listener() -> None:
"""Listen for new stay-out zones and add/remove switch entities if they did not exist."""
for mower_id in coordinator.data:
if (
coordinator.data[mower_id].capabilities.stay_out_zones
and (_stay_out_zones := coordinator.data[mower_id].stay_out_zones)
is not None
):
received_stay_out_zones = set(_stay_out_zones.zones)
current_stay_out_zones_set = current_stay_out_zones.get(mower_id, set())
new_stay_out_zones = (
received_stay_out_zones - current_stay_out_zones_set
)
removed_stay_out_zones = (
current_stay_out_zones_set - received_stay_out_zones
)
if new_stay_out_zones:
current_stay_out_zones.setdefault(mower_id, set()).update(
new_stay_out_zones
)
async_add_entities(
StayOutZoneSwitchEntity(
coordinator, mower_id, stay_out_zone_uid
)
for stay_out_zone_uid in new_stay_out_zones
)
if removed_stay_out_zones:
_remove_stay_out_zone_entities(removed_stay_out_zones, mower_id)
coordinator.async_add_listener(_async_work_area_listener)
coordinator.async_add_listener(_async_stay_out_zone_listener)
_async_work_area_listener()
_async_stay_out_zone_listener()
class AutomowerScheduleSwitchEntity(AutomowerControlEntity, SwitchEntity): class AutomowerScheduleSwitchEntity(AutomowerControlEntity, SwitchEntity):
@ -180,28 +233,3 @@ class WorkAreaSwitchEntity(WorkAreaControlEntity, SwitchEntity):
await self.coordinator.api.commands.workarea_settings( await self.coordinator.api.commands.workarea_settings(
self.mower_id, self.work_area_id, enabled=True self.mower_id, self.work_area_id, enabled=True
) )
@callback
def async_remove_entities(
hass: HomeAssistant,
coordinator: AutomowerDataUpdateCoordinator,
entry: AutomowerConfigEntry,
mower_id: str,
) -> None:
"""Remove deleted stay-out-zones from Home Assistant."""
entity_reg = er.async_get(hass)
active_zones = set()
_zones = coordinator.data[mower_id].stay_out_zones
if _zones is not None:
for zones_uid in _zones.zones:
uid = f"{mower_id}_{zones_uid}_stay_out_zones"
active_zones.add(uid)
for entity_entry in er.async_entries_for_config_entry(entity_reg, entry.entry_id):
if (
entity_entry.domain == Platform.SWITCH
and (split := entity_entry.unique_id.split("_"))[0] == mower_id
and split[-1] == "zones"
and entity_entry.unique_id not in active_zones
):
entity_reg.async_remove(entity_entry.entity_id)

View File

@ -1,6 +1,6 @@
"""Tests for init module.""" """Tests for init module."""
from datetime import timedelta from datetime import datetime, timedelta
import http import http
import time import time
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
@ -10,15 +10,17 @@ from aioautomower.exceptions import (
AuthException, AuthException,
HusqvarnaWSServerHandshakeError, HusqvarnaWSServerHandshakeError,
) )
from aioautomower.model import MowerAttributes from aioautomower.model import MowerAttributes, WorkArea
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
import pytest import pytest
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
from homeassistant.components.husqvarna_automower.const import DOMAIN, OAUTH2_TOKEN from homeassistant.components.husqvarna_automower.const import DOMAIN, OAUTH2_TOKEN
from homeassistant.components.husqvarna_automower.coordinator import SCAN_INTERVAL
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.util import dt as dt_util
from . import setup_integration from . import setup_integration
from .const import TEST_MOWER_ID from .const import TEST_MOWER_ID
@ -26,6 +28,10 @@ from .const import TEST_MOWER_ID
from tests.common import MockConfigEntry, async_fire_time_changed from tests.common import MockConfigEntry, async_fire_time_changed
from tests.test_util.aiohttp import AiohttpClientMocker from tests.test_util.aiohttp import AiohttpClientMocker
ADDITIONAL_NUMBER_ENTITIES = 1
ADDITIONAL_SENSOR_ENTITIES = 2
ADDITIONAL_SWITCH_ENTITIES = 1
async def test_load_unload_entry( async def test_load_unload_entry(
hass: HomeAssistant, hass: HomeAssistant,
@ -163,29 +169,6 @@ async def test_device_info(
assert reg_device == snapshot assert reg_device == snapshot
async def test_workarea_deleted(
hass: HomeAssistant,
mock_automower_client: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
values: dict[str, MowerAttributes],
) -> None:
"""Test if work area is deleted after removed."""
await setup_integration(hass, mock_config_entry)
current_entries = len(
er.async_entries_for_config_entry(entity_registry, mock_config_entry.entry_id)
)
del values[TEST_MOWER_ID].work_areas[123456]
mock_automower_client.get_status.return_value = values
await hass.config_entries.async_reload(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert len(
er.async_entries_for_config_entry(entity_registry, mock_config_entry.entry_id)
) == (current_entries - 2)
async def test_coordinator_automatic_registry_cleanup( async def test_coordinator_automatic_registry_cleanup(
hass: HomeAssistant, hass: HomeAssistant,
mock_automower_client: AsyncMock, mock_automower_client: AsyncMock,
@ -219,3 +202,70 @@ async def test_coordinator_automatic_registry_cleanup(
len(dr.async_entries_for_config_entry(device_registry, entry.entry_id)) len(dr.async_entries_for_config_entry(device_registry, entry.entry_id))
== current_devices - 1 == current_devices - 1
) )
async def test_add_and_remove_work_area(
hass: HomeAssistant,
mock_automower_client: AsyncMock,
mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
entity_registry: er.EntityRegistry,
values: dict[str, MowerAttributes],
) -> None:
"""Test adding a work area in runtime."""
await setup_integration(hass, mock_config_entry)
entry = hass.config_entries.async_entries(DOMAIN)[0]
current_entites_start = len(
er.async_entries_for_config_entry(entity_registry, entry.entry_id)
)
values[TEST_MOWER_ID].work_area_names.append("new work area")
values[TEST_MOWER_ID].work_area_dict.update({1: "new work area"})
values[TEST_MOWER_ID].work_areas.update(
{
1: WorkArea(
name="new work area",
cutting_height=12,
enabled=True,
progress=12,
last_time_completed=datetime(
2024, 10, 1, 11, 11, 0, tzinfo=dt_util.get_default_time_zone()
),
)
}
)
mock_automower_client.get_status.return_value = values
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
current_entites_after_addition = len(
er.async_entries_for_config_entry(entity_registry, entry.entry_id)
)
assert (
current_entites_after_addition
== current_entites_start
+ ADDITIONAL_NUMBER_ENTITIES
+ ADDITIONAL_SENSOR_ENTITIES
+ ADDITIONAL_SWITCH_ENTITIES
)
values[TEST_MOWER_ID].work_area_names.remove("new work area")
del values[TEST_MOWER_ID].work_area_dict[1]
del values[TEST_MOWER_ID].work_areas[1]
values[TEST_MOWER_ID].work_area_names.remove("Front lawn")
del values[TEST_MOWER_ID].work_area_dict[123456]
del values[TEST_MOWER_ID].work_areas[123456]
del values[TEST_MOWER_ID].calendar.tasks[:2]
mock_automower_client.get_status.return_value = values
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
current_entites_after_deletion = len(
er.async_entries_for_config_entry(entity_registry, entry.entry_id)
)
assert (
current_entites_after_deletion
== current_entites_start
- ADDITIONAL_SWITCH_ENTITIES
- ADDITIONAL_NUMBER_ENTITIES
- ADDITIONAL_SENSOR_ENTITIES
)

View File

@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, patch
import zoneinfo import zoneinfo
from aioautomower.exceptions import ApiException from aioautomower.exceptions import ApiException
from aioautomower.model import MowerAttributes, MowerModes from aioautomower.model import MowerAttributes, MowerModes, Zone
from aioautomower.utils import mower_list_to_dictionary_dataclass from aioautomower.utils import mower_list_to_dictionary_dataclass
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
import pytest import pytest
@ -38,8 +38,9 @@ from tests.common import (
snapshot_platform, snapshot_platform,
) )
TEST_ZONE_ID = "AAAAAAAA-BBBB-CCCC-DDDD-123456789101"
TEST_AREA_ID = 0 TEST_AREA_ID = 0
TEST_VARIABLE_ZONE_ID = "203F6359-AB56-4D57-A6DC-703095BB695D"
TEST_ZONE_ID = "AAAAAAAA-BBBB-CCCC-DDDD-123456789101"
async def test_switch_states( async def test_switch_states(
@ -179,6 +180,7 @@ async def test_work_area_switch_commands(
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory, freezer: FrozenDateTimeFactory,
mower_time_zone: zoneinfo.ZoneInfo, mower_time_zone: zoneinfo.ZoneInfo,
values: dict[str, MowerAttributes],
) -> None: ) -> None:
"""Test switch commands.""" """Test switch commands."""
entity_id = "switch.test_mower_1_my_lawn" entity_id = "switch.test_mower_1_my_lawn"
@ -219,26 +221,46 @@ async def test_work_area_switch_commands(
assert len(mocked_method.mock_calls) == 2 assert len(mocked_method.mock_calls) == 2
async def test_zones_deleted( async def test_add_stay_out_zone(
hass: HomeAssistant, hass: HomeAssistant,
mock_automower_client: AsyncMock, mock_automower_client: AsyncMock,
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
values: dict[str, MowerAttributes], values: dict[str, MowerAttributes],
) -> None: ) -> None:
"""Test if stay-out-zone is deleted after removed.""" """Test adding a stay out zone in runtime."""
await setup_integration(hass, mock_config_entry) await setup_integration(hass, mock_config_entry)
current_entries = len( entry = hass.config_entries.async_entries(DOMAIN)[0]
er.async_entries_for_config_entry(entity_registry, mock_config_entry.entry_id) current_entites = len(
er.async_entries_for_config_entry(entity_registry, entry.entry_id)
)
values[TEST_MOWER_ID].stay_out_zones.zones.update(
{
TEST_VARIABLE_ZONE_ID: Zone(
name="future_zone",
enabled=True,
)
}
) )
del values[TEST_MOWER_ID].stay_out_zones.zones[TEST_ZONE_ID]
mock_automower_client.get_status.return_value = values mock_automower_client.get_status.return_value = values
await hass.config_entries.async_reload(mock_config_entry.entry_id) freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
assert len( current_entites_after_addition = len(
er.async_entries_for_config_entry(entity_registry, mock_config_entry.entry_id) er.async_entries_for_config_entry(entity_registry, entry.entry_id)
) == (current_entries - 1) )
assert current_entites_after_addition == current_entites + 1
values[TEST_MOWER_ID].stay_out_zones.zones.pop(TEST_VARIABLE_ZONE_ID)
values[TEST_MOWER_ID].stay_out_zones.zones.pop(TEST_ZONE_ID)
mock_automower_client.get_status.return_value = values
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
current_entites_after_deletion = len(
er.async_entries_for_config_entry(entity_registry, entry.entry_id)
)
assert current_entites_after_deletion == current_entites - 1
async def test_switch_snapshot( async def test_switch_snapshot(